UNPKG

@twilio/plugin-microvisor

Version:

Interact with your Twilio Microvisor devices

441 lines (440 loc) 18.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.BaseCommand = void 0; exports.BaseCommand = require('@twilio/cli-core').baseCommands.BaseCommand; const { execSync } = require('child_process'); const { spawn } = require('child_process'); const fs = require('fs'); const path = require('path'); const core_1 = require("@oclif/core"); const process_1 = require("process"); class MicrovisorDeploy extends exports.BaseCommand { async run() { await super.run(); // Set flags and vars const projectPath = path.resolve(this.args.projectPath); const parts = projectPath.split('/'); const doGenkeys = this.flags["genkeys"]; const debugChoice = this.flags["cli-log-level"]; var doLog = this.flags["log"]; var doClean = this.flags["clean"]; var doDeploy = true && !this.flags["build"]; var passParams = { this: this, appName: (parts.pop() || parts.pop()), projPath: projectPath, deviceSid: this.flags["devicesid"], buildPath: path.join(projectPath, "build"), zipPath: "", binPath: "", pubkey: this.flags["publickey"], prvkey: this.flags["privatekey"], doDebug: (debugChoice != "none" && debugChoice != "info" && debugChoice != "undefined"), debugArg: " ", doBuild: true && !this.flags["deploy"], doUseDevKey: this.flags["development-keying"] }; // Pass along debug flag setting passParams.debugArg = passParams.doDebug ? `-l=${this.flags["cli-log-level"]}` : " "; // Only logging? Clear unwanted flags if (this.flags["logonly"]) { passParams.doBuild = false; doDeploy = false; doClean = false; doLog = true; } /* * Pre-flight checks */ // Check presence of cmake etc. checkReqs(this); // Check deploy option consistency if (this.flags["build"] && this.flags["deploy"]) { showErrorThenExit(this, `You cannot specify build only (--build) and deploy only (--deploy)`); } // Check that the project dir exists and is a directory if (!checkDirectory(projectPath)) showErrorThenExit(this, `${projectPath} is not a directory or does not exist`); // Was a valid remote debugging public key passed? (all we need to bundle for RD) if (passParams.pubkey != "NONE" && !checkFile(passParams.pubkey) && !doGenkeys) { showErrorThenExit(this, `Public key cannot be found at ${passParams.pubkey}`); } // Check we have a valid device SID -- can't assign the app to a device without one if (doDeploy && (passParams.deviceSid == "" || !passParams.deviceSid.startsWith("UV"))) { showErrorThenExit(this, "Could not assign app: no valid device SID set as an environment variable"); } /* * Debug */ if (passParams.doDebug) { this.logger.debug(`Project Path: ${projectPath}`); this.logger.debug(`App Name: ${passParams.appName}`); this.logger.debug(`Device: ${passParams.deviceSid}`); this.logger.debug(`Private key path: ${passParams.prvkey}`); this.logger.debug(`Public key path: ${passParams.pubkey}`); if (doDeploy && !passParams.doBuild) this.logger.debug("Deploy only"); if (!doDeploy && passParams.doBuild) this.logger.debug("Build only"); if (doDeploy && passParams.doBuild) this.logger.debug("Build and deploy"); } /* * Build prep */ // Check that there's a build directory -- make one if not // NOTE Making the dir uses cmake to initialize it if (!checkDirectory(passParams.buildPath)) { if (checkFile(passParams.buildPath)) { showErrorThenExit(this, "Could not create build directory: it exists as a file"); } try { // Initialize a cmake build -- creates directory 'build' execSync(`cmake -S ${projectPath} -B ${passParams.buildPath}`); } catch (err) { showErrorThenExit(this, `Could not create build directory: cmake error: ${err}`); } } /* * Build and bundle the app */ if (passParams.doBuild) { // Did the user ask to generate keys if (doGenkeys) { this.logger.info("Generating Remote Debugging keys..."); // Public key path may not be specified -- if so use the default if (passParams.pubkey == "NONE") passParams.pubkey = path.join(passParams.buildPath, "debug_auth_pub_key.pem"); if (passParams.prvkey == "NONE") passParams.prvkey = path.join(passParams.buildPath, "debug_auth_prv_key.pem"); // Check that passed (default or otherwise) key paths are not directories if (checkDirectory(passParams.pubkey)) passParams.pubkey = path.join(passParams.pubkey, "debug_auth_pub_key.pem"); if (checkDirectory(passParams.prvkey)) passParams.prvkey = path.join(passParams.prvkey, "debug_auth_prv_key.pem"); // Generate the keys try { // Use --force to mandate overwrites execSync(`twilio microvisor:debug:generate_keypair --debug-auth-privkey="${passParams.prvkey}" --debug-auth-pubkey="${passParams.pubkey}" --force ${passParams.debugArg}`); this.logger.info(`Private key written to ${passParams.prvkey}`); this.logger.info(` Public key written to ${passParams.pubkey}`); } catch (err) { showErrorThenExit(this, `Could not generate keys: ${err}`); } } // Increment build counter if we can updateBuildNumber(passParams, 1); // Start the build this.logger.info(`Building ${passParams.appName}...`); var args = ["--build", `${passParams.buildPath}`]; if (doClean) args.push("--clean-first"); // Spawn CMake const child = spawn("cmake", args); // Get stdout child.stdout.setEncoding("utf8"); child.stdout.on('data', (chunk) => { const lines = chunk.split('\r'); for (var line of lines) { if (line.endsWith('\n')) line = line.slice(0, line.length - 1); if (line.endsWith('\r')) line = line.slice(0, line.length - 1); this.logger.info(line); } }); // Get stderr child.stderr.setEncoding("utf8"); child.stderr.on('data', (chunk) => { const lines = chunk.split('\r'); for (var line of lines) { if (line.endsWith('\n')) line = line.slice(0, line.length - 1); if (line.endsWith('\r')) line = line.slice(0, line.length - 1); this.logger.error(line); } }); child.on('error', (code) => { showErrorThenExit(this, "Could not start the build process -- is CMake installed?"); }); // Exit build process child.on('exit', (code) => { // Async completion of build -- first check for failure if (code !== 0) showErrorThenExit(this, `Build failed (CMake exit code: ${code})`); // Bundle the app then deploy and log as required passParams = makeBundle(passParams); if (doDeploy) deployApp(passParams); if (doLog) logApp(passParams); }); return; } /* * Deploy the app as required */ if (doDeploy) deployApp(passParams); /* * Initiate logging as required */ if (doLog) logApp(passParams); } async runCommand() { return run(); } } exports.default = MicrovisorDeploy; MicrovisorDeploy.description = "Build and/or deploy a Microvisor application using the standard Microvisor CMake-based workflow"; MicrovisorDeploy.args = [ { name: 'projectPath', required: true, description: 'The path to the project directory' } ]; function showErrorThenExit(base, msg) { base.logger.info(`[ERROR] ${msg}`); (0, process_1.exit)(1); } function checkDirectory(path) { try { return fs.lstatSync(path).isDirectory(); } catch (e) { return false; } } function checkFile(path) { try { return fs.lstatSync(path).isFile(); } catch (e) { return false; } } function locateElf(aPath) { const files = fs.readdirSync(aPath); for (var i = 0; i < files.length; i++) { var file = path.join(aPath, files[i]); const stat = fs.statSync(file); if (stat && stat.isDirectory()) file = locateElf(file); if (file.endsWith(".elf")) return file; } return ""; } function makeBundle(params) { // Update path values params.buildPath = path.dirname(locateElf(params.buildPath)); params.zipPath = path.join(params.buildPath, `${params.appName}.zip`); params.binPath = path.join(params.buildPath, "*.bin"); if (params.doDebug) { params.this.logger.debug(`.bin Path: ${params.binPath}`); params.this.logger.debug(`.zip Name: ${params.zipPath}`); } params.this.logger.info(`Bundling ${params.appName}...`); // Support development keying let dev_key = params.doUseDevKey ? "--development-keying" : " "; // Bundle the app try { // Bundle the app with or without the remote debug key if (params.pubkey != "NONE") { execSync(`twilio microvisor:apps:bundle ${params.binPath} ${params.zipPath} --debug-auth-pubkey=${params.pubkey} ${dev_key} ${params.debugArg}`); } else { execSync(`twilio microvisor:apps:bundle ${params.binPath} ${params.zipPath} ${dev_key} ${params.debugArg}`); } } catch (err) { showErrorThenExit(params.this, `Could not bundle the build: ${err}`); } return params; } function deployApp(params) { // Didn't do a build? Calculate the paths we need if (!params.doBuild) { params.buildPath = path.dirname(locateElf(params.buildPath)); params.zipPath = path.join(params.buildPath, `${params.appName}.zip`); if (params.doDebug) { params.this.logger.debug(`.zip Name: ${params["zipPath"]}`); } // Check there's a build artefact to upload if (!checkFile(params.zipPath)) showErrorThenExit(params.this, `No build at ${params.zipPath} to upload`); } // Upload the build params.this.logger.info(`Uploading ${params.zipPath}...`); var appSid = ""; try { // Upload app and extract its SID from the response var shellOutput = String(execSync(`twilio microvisor:apps:create ${params.zipPath} -o=json ${params.debugArg}`)); const data = JSON.parse(shellOutput.replace("\n", "")); appSid = data[0]["sid"]; if (appSid == "" || appSid == "null") showErrorThenExit(params.this, "Could not upload app"); } catch (err) { showErrorThenExit(params.this, `Could not upload the app bundle: ${err}`); } params.this.logger.info(`Assigning app ${appSid} to device ${params.deviceSid}...`); try { // Assign the app to the device execSync(`twilio api:microvisor:v1:devices:update --sid=${params.deviceSid} --target-app=${appSid} ${params.debugArg}`); } catch (err) { showErrorThenExit(params.this, `Could not assign the app bundle: ${err}`); } // Remote debugging? Give the user activation info // TODO Figure out how to spawn this in a new terminal window... if (params.pubkey != "NONE" && params.prvkey != "NONE") { params.this.logger.info(`Use the following command to initiate remote debugging: twilio microvisor:debug ${params.deviceSid} \"${params.prvkey}\"`); } } function logApp(params) { // Trap ctrl-c during logging process.on('SIGINT', function () { params.this.logger.info("\nEnd of line"); process.exit(); }); // Initiate logging -- this is async and will only halt on error or ctrl-c params.this.logger.info(`Logging from ${params.deviceSid}...`); const child = spawn("twilio", ["microvisor:logs:stream", `${params.deviceSid}`]); child.stdout.setEncoding("utf8"); child.stdout.on('data', (chunk) => { const lines = chunk.split('\n\r'); for (var line of lines) { if (line.endsWith('\n')) line = line.slice(0, line.length - 1); params.this.logger.info(line); } }); } function checkReqs(base) { // Dependency checking: list external CLI tools // in the following array let platform = process.platform; let reqs = ["cmake"]; reqs.forEach(function (req) { try { if (platform == "linux" || platform == "darwin") { execSync(`which ${req}`); } else if (platform == "win32") { let result = String(execSync(`where ${req}`)); if (result.includes("Could not find files for the given pattern")) { throw new Error("error"); } } else { showErrorThenExit(base, `This platform is unsupported`); } } catch (err) { showErrorThenExit(base, `Required tool ${req} not installed`); } }); } function updateBuildNumber(base, delta) { // Looks for the `set(BUILD_NUMBER "x")` line in the app-level // CMakeLists.txt file, and increments the value of `x` // Locate the CMakeLists.txt file, bailing if there isn't one var cMakePath = findCmakeLists(base, base.projPath); if (cMakePath == "") return; try { // Read in the file, make the changes, and write out var data = fs.readFileSync(cMakePath, 'utf8'); var buildCount = 1; var updatedString = ""; const start = data.indexOf('(BUILD_NUMBER '); const end = data.indexOf('"', start + 15); if (end - start - 15 > 0) { const num = +data.substring(start + 15, end); buildCount = Number.isNaN(num) ? 1 : num + delta; } base.this.logger.info(`Incrementing build number to ${buildCount}`); updatedString = data.replace(/set\(BUILD_NUMBER \"[0-9a-zA-Z\s]*\"/, `set(BUILD_NUMBER "${buildCount}"`); fs.writeFileSync(cMakePath, updatedString); } catch (err) { base.this.logger.error("Could not increment the build count"); } } function findCmakeLists(base, aPath) { // Recursively locate a CMakeLists.txt file containing // the signature of the application build config file const signature = "set(BUILD_NUMBER "; const directoryFiles = fs.readdirSync(aPath); for (var i = 0; i < directoryFiles.length; i++) { var file = path.join(aPath, directoryFiles[i]); const stat = fs.statSync(file); if (stat && stat.isDirectory()) { // Bypass known ignorable directories if (file.endsWith("FreeRTOS-Kernel") || file.endsWith("ST_Code") || file.endsWith("twilio-microvisor-hal-stm32u5") || file.endsWith(".git")) { continue; } // Probe the directory file = findCmakeLists(base, file); } if (file.endsWith("CMakeLists.txt")) { try { // Read the file and check for the signature const data = fs.readFileSync(file, 'utf8'); if (data.indexOf(signature) != -1) return file; } catch (err) { // NOP -- errors handled by caller } } } return ""; } MicrovisorDeploy.flags = Object.assign({ 'privatekey': core_1.Flags.string({ description: 'Path to your private key', default: 'NONE' }), 'publickey': core_1.Flags.string({ description: 'Path to your public key', default: 'NONE' }), 'devicesid': core_1.Flags.string({ description: 'The SID of the device to which you will upload the build', default: 'NONE' }), 'clean': core_1.Flags.boolean({ description: 'Clean the build folder first', default: false, char: "c" }), 'build': core_1.Flags.boolean({ description: 'Build without deploying', default: false, char: "b" }), 'deploy': core_1.Flags.boolean({ description: 'Deploy the most recent build without rebuilding', default: false, char: "d" }), 'log': core_1.Flags.boolean({ description: 'Start logging after a deploy and/or build', default: false }), 'logonly': core_1.Flags.boolean({ description: 'Start logging immediately without building or deploying', default: false }), 'genkeys': core_1.Flags.boolean({ description: 'Generate remote debugging keys', default: false }), 'development-keying': core_1.Flags.boolean({ default: false, hidden: true }) }, exports.BaseCommand.flags); // Remove some unnecessary options delete MicrovisorDeploy.flags['cli-output-format']; delete MicrovisorDeploy.flags.silent; delete MicrovisorDeploy.flags.profile;