UNPKG

@webos-tools/cli

Version:

Command Line Interface for development webOS application and service

608 lines (547 loc) 24.8 kB
/* * Copyright (c) 2020-2024 LG Electronics Inc. * * SPDX-License-Identifier: Apache-2.0 */ const async = require('async'), chalk = require('chalk'), exec = require('child_process').exec, fs = require('fs'), npmlog = require('npmlog'), os = require('os'), path = require('path'), semver = require('semver'), util = require('util'), fileWatcher = require('./base/file-watcher'), inspector = require('./inspect'), installer = require('./install'), sessionLib = require('./session'), commonTools = require('./base/common-tools'), errHndl = require('./base/error-handler'), luna = require('./base/luna'), novacom = require('./base/novacom'), spinner = require('./util/spinner'); const appdata = commonTools.appdata, sdkenv = commonTools.sdkenv; (function() { const log = npmlog; log.heading = 'launcher'; log.level = 'warn'; const launcher = { /** * @property {Object} log an npm log instance */ log: log, /** * Launch the given application id * @param {Object} options * @property options {String} device the device to connect to * @property options {Boolean} inspect run the application with web-inspector turned on */ launch: function(options, id, params, next, middleCb) { if (typeof next !== 'function') { throw errHndl.getErrMsg("MISSING_CALLBACK", "next", util.inspect(next)); } const self = this, currentProfile = appdata.getConfig(true).profile, hostedAppId = currentProfile === "signage" ? "com.lg.app.signage.dev" : "com.sdk.ares.hostedapp", hostedAppIpkName = currentProfile === "signage" ? "com.sdk.ares.signage.hostedapp" : "com.sdk.ares.hostedapp"; let hostedAppInstalled = false; options = options || {}; async.series([ _checkInstalledApp, _checkRunningApp, _installHostedApp, _makeSession, _runAppServer, _setAppServerInfo, _getSessionList, _checkDisplayAffinity, _launch, _runFileWatcher, _runInspector ], function(err, results) { log.silly("launch#launch()", "err:", err, ", results:", results); const result = results[8]; if (!err) { if (options.installMode !== "Hosted") { result.msg = "Launched application " + id; if (self.displayId !== undefined) { result.msg += " on display " + self.displayId; } } else { options.session = null; options.printTarget = false; self.close(options, hostedAppId, params, next); result.msg = ""; return; } } next(err, result); }); function _checkInstalledApp(next) { if (options.installMode === "Hosted") { installer.list(options, function(err, result) { for (const index in result) { if (result[index].id === hostedAppId) { hostedAppInstalled = true; break; } } next(err); }); } else { next(); } } function _checkRunningApp(next) { if (options.installMode === "Hosted") { self.listRunningApp(options, function(err, result) { for (const index in result) { if (result[index].id === hostedAppId) { self.close(options, hostedAppId, params, next); return; } } next(err); }); } else { next(); } } function _installHostedApp(next) { if (options.installMode === "Hosted" && !hostedAppInstalled) { const hostedAppUrl = path.join(__dirname, hostedAppIpkName + ".ipk"); options.appId = id; installer.install(options, hostedAppUrl, next, function(value) { middleCb(value); }); } else { next(); } } function _makeSession(next) { makeSession(options, next); } function _runAppServer(next) { if (options.installMode === "Hosted") { options.session.runHostedAppServer(options.hostedurl, next); spinner.stop(); middleCb("Ares Hosted App is now running..."); } else { next(); } } function _setAppServerInfo(next) { if (options.installMode === "Hosted") { if (options.hostIp) { options.localIP = options.hostIp; } else { const networkInterfaces = os.networkInterfaces(); let localIP = ""; for (const devName in networkInterfaces) { for (let index = 0; index < networkInterfaces[devName].length; index++) { const alias = networkInterfaces[devName][index]; if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) { localIP = localIP || alias.address; options.localIP = localIP; } } } } const port = options.session.getHostedAppServerPort(); if (params === null) { params = {}; } params.hostedurl = "http://" + options.localIP + ":" + port + "/"; } next(); } function _getSessionList(next) { sessionLib.getSessionList(options, next); } function _checkDisplayAffinity(next) { checkDisplayAffinity(options, params, next); } function _launch(next) { const target = options.session.getDevice(), addr = target.lunaAddr.launch, returnValue = addr.returnValue.split('.'), param = { // luna param id: id, subscribe: false, params: params }; luna.send(options, addr, param, function(lineObj, next) { let resultValue = lineObj; for (let index = 1; index < returnValue.length; index++) { resultValue = resultValue[returnValue[index]]; } if (resultValue) { // success: stop log.verbose("launch#launch()#_launch()", "success"); // If sam returns "displayId" show displayId in result msg if (lineObj.displayId !== undefined) { self.displayId = lineObj.displayId; } next(null, {procId: resultValue}); } else { // failure: stop log.verbose("launch#launch()#_launch()", "failure"); next(errHndl.getErrMsg("INVALID_OBJECT")); } }, next); } function _runFileWatcher(next) { if (options.installMode === "Hosted") { log.verbose("launcher#launch#_runFileWatcher()"); options.appId = id; fileWatcher.watch(options, params); next(); } else { next(); } } function _runInspector(next) { if (options.inspect) { options.appId = id; options.running = true; async.series([ inspector.inspect.bind(inspector, options, null), function() { // TODO: hold process to keep alive } ], function(err) { next(err); }); } else if (options.installMode === "Hosted") { process.on("SIGINT", function() { log.verbose("launcher#launch#_runInspector():", "SIGINT is detected in Hosted mode"); next(); }); } else { next(); } } }, /** * Close the given application id * @param {Object} options * @property options {String} device the device to connect to * @property options {Boolean} inspect run the application with web-inspector turned on */ close: function(options, id, params, next) { if (typeof next !== 'function') { throw errHndl.getErrMsg("MISSING_CALLBACK", "next", util.inspect(next)); } const self = this; options = options || {}; async.series([ _makeSession, _getSessionList, _checkDisplayAffinity, _close ], function(err, results) { log.silly("launch#close()", "err:", err, ", results:", results); // 2 steps in async.series, we want to // the value returned by the second // step (index=1) const result = results[3]; if (result) { if (options.installMode !== "Hosted") { result.msg = "Closed application " + id; if (self.displayId !== undefined) { result.msg += " on display " + self.displayId; } } else { result.msg = "...Closed Ares Hosted App"; } } next(err, result); }); function _makeSession(next) { makeSession(options, next); } function _getSessionList(next) { sessionLib.getSessionList(options, next); } function _checkDisplayAffinity(next) { checkDisplayAffinity(options, params, next); } function _close(next) { const target = options.session.getDevice(), addr = target.lunaAddr.terminate, returnValue = addr.returnValue.split('.'), param = { // luna param id: id, subscribe: false, params: params }; luna.send(options, addr, param, function(lineObj, next) { let resultValue = lineObj; for (let index = 1; index < returnValue.length; index++) { resultValue = resultValue[returnValue[index]]; } if (resultValue) { // success: stop log.verbose("launch#close()#_close()", "success"); // If sam returns "displayId" show displayId in result msg if (lineObj.displayId !== undefined) { self.displayId = lineObj.displayId; } next(null, {procId: resultValue}); } else { // failure: stop log.verbose("launch#close()#_close()", "failure"); next(errHndl.getErrMsg("INVALID_OBJECT")); } }, next); } }, /** * list the running applications * @param {Object} options * @property options {String} device the device to connect to * @property options {Boolean} inspect run the application with web-inspector turned on */ listRunningApp: function(options, next) { if (typeof next !== 'function') { throw errHndl.getErrMsg("MISSING_CALLBACK", "next", util.inspect(next)); } options = options || {}; async.series([ _makeSession, _getSessionList, _listRunningApp ], function(err, results) { log.silly("launch#listRunningApp()", "err:", err, ", results:", results); next(err, results[2]); }); function _makeSession(next) { makeSession(options, next); } function _getSessionList(next) { sessionLib.getSessionList(options, next); } function _listRunningApp(next) { const target = options.session.getDevice(), addr = target.lunaAddr.running, returnValue = addr.returnValue.split('.'), param = { // luna param subscribe: false }; if (!addr || !returnValue) { return next(errHndl.getErrMsg("NOT_SUPPORT_RUNNINGLIST")); } luna.send(options, addr, param, function(lineObj, next) { let resultValue = lineObj; for (let index = 1; index < returnValue.length; index++) { resultValue = resultValue[returnValue[index]]; } resultValue = resultValue || []; if (lineObj.returnValue) { // success: stop log.verbose("launch#listRunningApp()", "success"); next(null, resultValue); } else { // failure: stop log.verbose("launch#listRunningApp()", "failure"); next(errHndl.getErrMsg("INVALID_OBJECT")); } }, next); } }, /** * Launch the given application directory to the simulator * @param {Object} options * @property options {String} simulator the webOS TV version of simulator to launch * @param {String} appDir the path of application directory * @param {Object} params parameters to launch with */ launchSimulator: function(options, appDir, params, next, middleCb) { if (typeof next !== 'function') { throw errHndl.getErrMsg("MISSING_CALLBACK", "next", util.inspect(next)); } options = options || {}; options.simulatorPrefix = `webOS_TV_${options.simulator}_Simulator`; async.waterfall([ _checkSimulatorConfig, _checkEnvPath, _checkSimulatorDirectory, _findSimulatorVersion, _launchSimulator ], function(err, results) { next(err, results); }); function _checkSimulatorConfig(next) { const configPath = appdata.getSimulatorConfigPath(); let cofigData = {}; if (fs.existsSync(configPath)) { cofigData = JSON.parse(fs.readFileSync(configPath)); } if (options.simulatorPath) { cofigData[options.simulator] = options.simulatorPath; try { fs.writeFileSync(configPath, JSON.stringify(cofigData, null, Number(4)) + "\n"); } catch(err) { next(err, null); } } Object.keys(cofigData).forEach(function(version) { if (version === options.simulator) { options.simulatorDir = cofigData[options.simulator]; } }); next(null, options.simulatorDir); } function _checkEnvPath(simulatorDir, next) { log.silly("launcher#launchSimulator#_checkEnvPath():", "simulatorDir:", simulatorDir); if (!simulatorDir) { sdkenv.getEnvValue("SDK", function(err, sdkHomePath) { if (sdkHomePath) { middleCb(chalk.yellow("[WARNING] Use <--simulator-path> option instead of environment variable <LG_WEBOS_TV_SDK_HOME>")); simulatorDir = path.join(sdkHomePath, "Simulator"); if (fs.existsSync(simulatorDir)) { if (fs.statSync(simulatorDir).isDirectory()) { const dirList = fs.readdirSync(simulatorDir); for (let i = 0; i < dirList.length; i++) { if (dirList[i].indexOf(options.simulatorPrefix) === 0) { const subDirPath = path.resolve(simulatorDir, dirList[i]); if (fs.statSync(subDirPath).isDirectory()) { options.simulatorDir = subDirPath; } } } if (options.simulatorDir) { next(null, options.simulatorDir); } else { next(errHndl.getErrMsg('EMPTY_VALUE', "--simulator-path")); } } else { next(errHndl.getErrMsg('NOT_DIRTYPE_PATH', simulatorDir)); } } else { next(errHndl.getErrMsg('NOT_EXIST_PATH', simulatorDir)); } } else { next(errHndl.getErrMsg('EMPTY_VALUE', "--simulator-path")); } }); } else { next(null, options.simulatorDir); } } function _checkSimulatorDirectory(dirPath, next) { log.silly("launcher#launchSimulator#_checkSimulatorDirectory():", "dirPath:", dirPath); if (fs.existsSync(dirPath)) { if (fs.statSync(dirPath).isDirectory()) { middleCb("Finding simulator in " + dirPath); next(null, fs.readdirSync(dirPath)); } else { next(errHndl.getErrMsg('NOT_DIRTYPE_PATH', dirPath)); } } else { next(errHndl.getErrMsg('NOT_EXIST_PATH', dirPath)); } } function _findSimulatorVersion(fileList, next) { log.silly("launcher#launchSimulator#_findSimulatorVersion():", "fileList:", fileList); let versionList = []; for (let i = 0; i < fileList.length; i++) { if (fileList[i].indexOf(options.simulatorPrefix) === 0) { const filePath = path.resolve(options.simulatorDir, fileList[i]); if (fs.existsSync(filePath) && [".exe", ".appimage", ".app"].includes(path.extname(filePath))) { versionList.push(path.parse(filePath).name.slice(options.simulatorPrefix.length + 1)); } } } if (versionList.length === 0) { next(errHndl.getErrMsg("NOT_EXIST_SIMULATOR", options.simulatorPrefix)); } else { try { // sort version descending order versionList = versionList.sort(semver.rcompare); options.simulatorVersion = versionList[0]; next(); } catch(err) { next(err, null); } } } function _launchSimulator(next) { let ext = ''; switch (process.platform) { case 'win32': ext = 'exe'; break; case 'linux': ext = 'appimage'; break; case 'darwin': ext = 'app'; break; default: } const simulatorName = `${options.simulatorPrefix}_${options.simulatorVersion}`, simulatorPath = path.join(options.simulatorDir, `${simulatorName}.${ext}`); if (!fs.existsSync(simulatorPath)) { return setImmediate(next, errHndl.getErrMsg('NOT_EXIST_PATH', simulatorPath)); } const paramsStr = JSON.stringify(params).replace(/"/g, "\\\""), cmd = process.platform === 'darwin' ? `open "${simulatorPath}" --args "${appDir}" "${paramsStr}"` : `"${simulatorPath}" "${appDir}" "${paramsStr}"`; log.info("launcher#launchSimulator#_launchSimulator():", "cmd:", cmd); exec(cmd, function(err) { if (err) { return setImmediate(next, err); } }); // exec callback is called when the simulator is closed. // Call next callback if there is no error for 1 second. setTimeout(function() { next(null, {msg: "Launched " + path.basename(simulatorPath)}); }, 1000); } } }; function makeSession(options, next) { options.nReplies = 1; // -n 1 if (!options.session) { log.info("launch#makeSession()", "need to make new session"); const printTarget = (options.printTarget === false) ? options.printTarget : true; options.session = new novacom.Session(options.device, printTarget, next); } else { log.info("launch#makeSession()", "already exist session"); next(); } } function checkDisplayAffinity(options, params, next) { // case of do not need to call session call(ose), check displayAffinity with display if (!options.sessionCall) { if (params && params.displayAffinity !== undefined && params.displayAffinity !== null) { if (typeof(params.displayAffinity) === 'string') { next(errHndl.getErrMsg("INVALID_DISPLAY")); } if (options && options.display && (Number(options.display) !== params.displayAffinity)) { if (Number(options.display) !== params.displayAffinity) { next(errHndl.getErrMsg("UNMATCHED_DISPLAY_AFFINITY")); } } } else { params.displayAffinity = 0; if (options && options.display) { params.displayAffinity = Number(options.display); } } } next(null, {}); } if (typeof module !== 'undefined' && module.exports) { module.exports = launcher; } }());