@webos-tools/cli
Version:
Command Line Interface for development webOS application and service
574 lines (536 loc) • 18.4 kB
JavaScript
/*
* Copyright (c) 2020-2024 LG Electronics Inc.
*
* SPDX-License-Identifier: Apache-2.0
*/
const async = require('async'),
inquirer = require('inquirer'),
nopt = require('nopt'),
abbrev = require("abbrev"),
log = require('npmlog'),
path = require('path'),
Ssdp = require('ssdp-js'),
commonTools = require('./../lib/base/common-tools'),
novacom = require('./../lib/base/novacom');
const version = commonTools.version,
cliControl = commonTools.cliControl,
help = commonTools.help,
appdata = commonTools.appdata,
errHndl = commonTools.errMsg,
setupDevice = commonTools.setupDevice;
const processName = path.basename(process.argv[1]).replace(/.js/, '');
process.on('uncaughtException', function(err) {
log.error('uncaughtException', err.toString());
log.verbose('uncaughtException', err.stack);
cliControl.end(-1);
});
const knownOpts = {
// generic options
"help": Boolean,
"level": ['silly', 'verbose', 'info', 'http', 'warn', 'error'],
"version": Boolean,
// command-specific options
"list": Boolean,
"listfull": Boolean,
"add": [String, null],
"remove": [String, null],
"modify": [String, null],
"default": [String, null],
"search": Boolean,
"timeout": [String, null],
"info": [String, Array],
"reset": Boolean
};
const shortHands = {
// generic aliases
"h": ["--help"],
"v": ["--level", "verbose"],
"V": ["--version"],
// command-specific aliases
"l": ["--list"],
"F": ["--listfull"],
"i": ["--info"],
"a": ["--add"],
"r": ["--remove"],
"m": ["--modify"],
"f": ["--default"],
"s": ["--search"],
"t": ["--timeout"],
"R": ["--reset"]
};
const argv = getArgv();
log.heading = processName;
log.level = argv.level || 'warn';
log.verbose("argv", argv);
const inqChoices = ["add", "modify"],
dfChoices = ["set default"],
rmChoices = ["remove"],
totChoices = inqChoices.concat(rmChoices, dfChoices);
let questions = [],
op;
if (argv.list) {
op = deviceList;
} else if (argv.listfull) {
op = deviceListFull;
} else if (argv.reset) {
op = reset;
} else if (argv.search || argv.timeout) {
op = search;
} else if (argv.add || argv.modify || argv.info) {
op = modifyDeviceInfo;
} else if (argv.remove) {
op = removeDeviceInfo;
} else if (argv.default) {
op = setDefaultDeviceInfo;
} else if (argv.version) {
version.showVersionAndExit();
} else if (argv.help) {
help.display(processName, appdata.getConfig(true).profile);
cliControl.end();
} else {
op = interactiveInput;
}
if (op) {
version.checkNodeVersion(function() {
async.series([
op.bind(this)
],finish);
});
}
const _needInq = function(choice) {
return function(choices) {
return (choices.indexOf(choice) !== -1);
};
};
function deviceList() {
setupDevice.showDeviceList(finish);
}
function deviceListFull() {
setupDevice.showDeviceListFull(finish);
}
function reset() {
setupDevice.resetDeviceList(finish);
}
function _queryAddRemove(ssdpDevices, next) {
let selDevice = {};
const resolver = this.resolver || (this.resolver = new novacom.Resolver());
if (typeof ssdpDevices === 'function') {
next = ssdpDevices;
ssdpDevices = null;
}
async.waterfall([
resolver.load.bind(resolver),
resolver.list.bind(resolver),
function(devices, next) {
if (!ssdpDevices) return next(null, devices, null);
const ssdpDevMap = {};
ssdpDevices.forEach(function(sd) {
const key = sd.name + ' # '+sd.address;
for (const idx in devices) {
if (devices[idx].name === sd.name)
{
ssdpDevMap[key] = devices[idx];
ssdpDevMap[key].op = 'modify';
ssdpDevMap[key].host = sd.address;
break;
}
}
if (!ssdpDevMap[key]) {
ssdpDevMap[key] = {
name: sd.name,
host: sd.address,
op: 'add'
};
}
});
questions = [{
type: "list",
name: "discovered",
message: "Select",
choices: Object.keys(ssdpDevMap)
}];
inquirer.prompt(questions).then(function(answers) {
next(null, devices, ssdpDevMap[answers.discovered]);
});
},
function(devices, ssdpDevice, next) {
const deviceNames = devices.filter(function(device) {
return (device.conn.indexOf('novacom') === -1);
}).map(function(device) {
return (device.name);
});
questions = [{
type: "list",
name: "op",
message: "Select",
choices: function() {
if (ssdpDevice) {
if (ssdpDevice.op === 'modify') return inqChoices;
else return ['add'];
}
if (deviceNames.length > 1) return totChoices;
// deveice list has emulator only > unsupported remove option
return inqChoices.concat(dfChoices);
},
filter: function(val) {
return val.toLowerCase();
},
default: function() {
if (ssdpDevice && ssdpDevice.op) return ssdpDevice.op;
else return null;
}
}, {
type: "input",
name: "device_name",
message: "Enter Device Name:",
when: function(answers) {
return (answers.op === "add");
},
default: function() {
if (ssdpDevice && ssdpDevice.name) return ssdpDevice.name;
else return null;
},
validate: function(input) {
if (input.length < 1) {
return errHndl.getErrStr("EMPTY_VALUE");
}
if (deviceNames.indexOf(input) !== -1) {
return errHndl.getErrStr("EXISTING_VALUE");
}
if (!setupDevice.isValidDeviceName(input)) {
return errHndl.getErrStr("INVALID_DEVICENAME");
}
return true;
}
}, {
type: "list",
name: "device_name",
message: "Select a device",
choices: deviceNames.filter(dv => dv !== "emulator"),
when: function(answers) {
return (["remove"].indexOf(answers.op) !== -1 && !ssdpDevice);
}
}, {
type: "list",
name: "device_name",
message: "Select a device",
choices: deviceNames,
when: function(answers) {
return (["modify", "set default"].indexOf(answers.op) !== -1 && !ssdpDevice);
}
}];
inquirer.prompt(questions)
.then(function(answers) {
devices.forEach(function(device) {
if (answers.device_name === device.name) {
selDevice = device;
}
});
selDevice.name = answers.device_name || ((ssdpDevice) ? ssdpDevice.name : null);
selDevice.mode = answers.op || ((ssdpDevice) ? ssdpDevice.op : null);
selDevice.host = (ssdpDevice) ? ssdpDevice.host : (selDevice.host || null);
next(null, selDevice, null);
});
}
], function(err, result) {
next(err, result);
});
}
function _queryDeviceInfo(selDevice, next) {
let mode = selDevice.mode;
const deviceName = selDevice.name,
resolver = this.resolver || (this.resolver = new novacom.Resolver()),
defaultDeviceInfo = appdata.getConfig(true).defaultDeviceInfo;
questions = [{
type: "input",
name: "ip",
message: "Enter Device IP address:",
default: function() {
return selDevice.host || defaultDeviceInfo.ipAddress;
},
validate: function(answers) {
if (!setupDevice.isValidIpv4(answers)) {
return errHndl.getErrStr("INVALID_VALUE");
}
return true;
},
when: function() {
return _needInq(mode)(inqChoices);
}
}, {
type: "input",
name: "port",
message: "Enter Device Port:",
default: function() {
return selDevice.port || defaultDeviceInfo.port;
},
validate: function(answers) {
if (!setupDevice.isValidPort(answers)) {
return errHndl.getErrStr("INVALID_VALUE");
}
return true;
},
when: function() {
return _needInq(mode)(inqChoices);
}
}, {
type: "input",
name: "user",
message: "Enter ssh user:",
default: function() {
return selDevice.username || defaultDeviceInfo.user;
},
when: function() {
return _needInq(mode)(inqChoices);
}
}, {
type: "input",
name: "description",
message: "Enter description:",
default: function() {
return selDevice.description || defaultDeviceInfo.description;
},
when: function() {
return _needInq(mode)(inqChoices);
}
}, {
type: "list",
name: "auth_type",
message: "Select authentication",
choices: ["password", "ssh key"],
default: function() {
let idx = 0;
if (selDevice.privateKeyName) {
idx = 1;
}
return idx;
},
when: function(answers) {
return _needInq(mode)(inqChoices) && answers.user === "root";
}
}, {
type: "password",
name: "password",
message: "Enter password:",
when: function(answers) {
return _needInq(mode)(inqChoices) && (answers.auth_type === "password");
}
}, {
type: "input",
name: "ssh_key",
message: "Enter ssh private key file name:",
default: function() {
return selDevice.privateKeyName || "webos_emul";
},
when: function(answers) {
return _needInq(mode)(inqChoices) && (answers.auth_type === "ssh key");
}
}, {
type: "confirm",
name: "default",
message: "Set default ?",
default: false,
when: function() {
return (mode === "add");
}
}, {
type: "confirm",
name: "confirm",
message: "Save ?",
default: true
}];
inquirer.prompt(questions).then(function(answers) {
if (answers.confirm) {
log.info("interactiveInput()#_queryDeviceInfo()", "saved");
} else {
log.info("interactiveInput()#_queryDeviceInfo()", "canceled");
return next(null, {
"msg": "Canceled"
});
}
const inDevice = {
profile: appdata.getConfig(true).profile,
name: deviceName,
host: answers.ip,
port: answers.port,
description: answers.description,
username: answers.user,
default: answers.default
};
if (answers.user !== 'prisoner' && ["add", "modify"].includes(mode)) {
if (answers.auth_type && answers.auth_type === "password") {
inDevice.password = answers.password;
inDevice.privateKey = "@DELETE@";
inDevice.passphrase = "@DELETE@";
inDevice.privateKeyName = "@DELETE@";
} else if ((answers.auth_type && answers.auth_type === "ssh key") || answers.user === "developer") {
inDevice.password = "@DELETE@";
inDevice.privateKey = {
"openSsh": answers.ssh_key || "webos_emul"
};
inDevice.passphrase = answers.ssh_passphrase || "@DELETE@";
inDevice.privateKeyName = "@DELETE@";
} else {
return next(errHndl.getErrMsg("NOT_SUPPORT_AUTHTYPE", answers.auth_type));
}
}
if (mode === 'set default') {
inDevice.default = true;
mode = 'default';
}
setupDevice.replaceDefaultDeviceInfo(inDevice);
if (inDevice.port) {
inDevice.port = Number(inDevice.port);
}
async.series([
resolver.load.bind(resolver),
resolver.modifyDeviceFile.bind(resolver, mode, inDevice),
setupDevice.showDeviceList.bind(this),
], function(err, results) {
if (err) {
return next(err);
}
if(results[2] && results[2].msg){
console.log(results[2].msg);
}
if(inDevice.username === 'prisoner' && mode === 'add'){
setupDevice.displayGetKeyGuide(inDevice.name);
}
finish();
});
});
}
function interactiveInput() {
async.waterfall([
setupDevice.showDeviceList.bind(this),
function(data, next) {
console.log(data.msg);
console.log("** You can modify the device info in the above list, or add new device.");
next();
},
_queryAddRemove,
_queryDeviceInfo
], function(err, result) {
finish(err, result);
});
}
function search(next) {
const TIMEOUT = 5000,
ssdp = new Ssdp(),
timeout = Number(argv.timeout) * 1000 || TIMEOUT,
outterNext = next,
self = this;
let discovered = [],
end = false;
console.log("Searching...");
log.verbose("search()", "timeout:", timeout);
ssdp.start();
ssdp.onDevice(function(device) {
if (!device.headers || !device.headers.SERVER ||
device.headers.SERVER.indexOf('WebOS') < 0 || end) {
return finish(null, {msg: "No devices is discovered."});
}
log.verbose("search()# %s:%s (%s)", '[Discovered]', device.name, device.address);
});
async.waterfall([
function(next) {
setTimeout(function() {
discovered = ssdp.getList().map(function(device) {
end = true;
return {
"uuid": device.headers.USN.split(':')[1],
"name": device.name.replace(/\s/g, '_'),
"address": device.address,
"registered": false
};
});
// ssdp.destroy();
if (discovered.length === 0) {
console.log("No devices is discovered.");
return outterNext();
}
log.verbose("search()", "discovered:", discovered.length);
next(null, discovered);
}, timeout);
},
_queryAddRemove.bind(self),
_queryDeviceInfo.bind(self)
], function(err) {
next(err);
});
}
function modifyDeviceInfo() {
setupDevice.modifyDeviceInfo(argv, finish);
}
function setDefaultDeviceInfo() {
setupDevice.setDefaultDevice(argv.default, finish);
}
function removeDeviceInfo() {
setupDevice.removeDeviceInfo(argv, finish);
}
function finish(err, value) {
log.info("finish()");
if (err) {
// handle err from getErrMsg()
if (Array.isArray(err) && err.length > 0) {
for (const index in err) {
log.error(err[index].heading, err[index].message);
}
log.verbose(err[0].stack);
} else {
// handle general err (string & object)
log.error(err.toString());
log.verbose(err.stack);
}
cliControl.end(-1);
} else {
log.verbose("finish()", "value:", value);
if (value && value.msg) {
console.log(value.msg);
}
cliControl.end();
}
}
function getArgv() {
let argv = nopt(knownOpts, shortHands, process.argv, 2 /* drop 'node' & 'ares-*.js'*/);
let mode = checkMode(argv);
return (mode && argv[mode] === 'true') ? reParse(argv, mode) : argv;
}
function checkMode(argv) {
if (argv.add) return 'add';
if (argv.modify) return 'modify';
if (argv.remove) return 'remove';
return (argv.default) ? 'default' : null;
}
function reParse(argv, mode) {
let nameIndex = -1;
let cookedNameIndex = -1;
let abbrevOpts = abbrev(Object.keys(knownOpts))
for (let i = 0; i < argv.argv.original.length; i++) {
if (argv.argv.original[i].match(/^-{2,}$/)) {
nameIndex = i;
break;
}
}
cookedNameIndex = argv.argv.cooked.indexOf('--');
if (cookedNameIndex <= 0 || !argv.argv.cooked[cookedNameIndex - 1].match(/^-/) || abbrevOpts[argv.argv.cooked[cookedNameIndex - 1].replace(/^-+/, "")] != mode) {
return argv;
}
argv[mode] = argv.argv.original[nameIndex];
if (nameIndex != -1 && cookedNameIndex != -1 && nameIndex + 1 < argv.argv.original.length) {
const remainArgv = nopt(knownOpts, shortHands, argv.argv.original, nameIndex + 1);
for (const option in remainArgv) {
if (remainArgv.hasOwnProperty(option)) {
if (!argv.hasOwnProperty(option)) {
argv[option] = remainArgv[option];
} else if (Array.isArray(argv[option])) {
argv[option] = argv[option].concat(remainArgv[option]);
}
}
}
argv.argv.remain = remainArgv.argv.remain;
argv.argv.cooked = argv.argv.cooked.splice(0, cookedNameIndex + 1).concat(remainArgv.argv.cooked);
}
return argv
}