@twilio/plugin-microvisor
Version:
Interact with your Twilio Microvisor devices
441 lines (440 loc) • 18.1 kB
JavaScript
;
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;