particle-cli
Version:
Simple Node commandline application for working with your Particle devices and using the Particle Cloud
493 lines (434 loc) • 11.6 kB
JavaScript
;
const fs = require('fs-extra');
const _ = require('lodash');
const propertiesParser = require('properties-parser');
const os = require('os');
const path = require('path');
const glob = require('glob');
const VError = require('verror');
const childProcess = require('child_process');
const { PLATFORMS } = require('./platform');
const log = require('./log');
const { DeviceProtectionError } = require('particle-usb');
const archiver = require('archiver');
const { createHash } = require('crypto');
module.exports = {
deferredChildProcess(exec){
return new Promise((resolve, reject) => {
childProcess.exec(exec, (error, stdout) => {
if (error){
reject(error);
} else {
resolve(stdout);
}
});
});
},
deferredSpawnProcess(exec, args){
return new Promise((resolve, reject) => {
try {
log.verbose('spawning ' + exec + ' ' + args.join(' '));
const options = {
stdio: ['ignore', 'pipe', 'pipe']
};
const child = childProcess.spawn(exec, args, options);
const stdout = [],
errors = [];
if (child.stdout){
child.stdout.pipe(log.stdout());
child.stdout.on('data', (data) => {
stdout.push(data);
});
}
if (child.stderr){
child.stderr.pipe(log.stderr());
child.stderr.on('data', (data) => {
errors.push(data);
});
}
child.on('close', (code) => {
const output = { stdout: stdout, stderr: errors };
if (!code){
resolve(output);
} else {
reject(output);
}
});
} catch (ex){
console.error('Error during spawn ' + ex);
reject(ex);
}
});
},
// TODO (mirande): use util.promisify once node@6 is no longer supported
readFile(file, options){
return new Promise((resolve, reject) => {
fs.readFile(file, options, (error, data) => {
if (error){
return reject(error);
}
return resolve(data);
});
});
},
// TODO (mirande): use util.promisify once node@6 is no longer supported
writeFile(file, data, options){
return new Promise((resolve, reject) => {
fs.writeFile(file, data, options, error => {
if (error){
return reject(error);
}
return resolve();
});
});
},
filenameNoExt(filename){
if (!filename || (filename.length === 0)){
return filename;
}
return filename.replace(/\.[^/.]+$/, '');
},
getFilenameExt(filename){
if (!filename || (filename.length === 0)){
return filename;
}
filename = filename.toString();
const ext = filename.match(/\.[^/.]+$/);
return ext ? ext.toString().toLowerCase() : '';
},
// TODO (mirande): replace w/ @particle/async-utils
delay(ms){
return new Promise((resolve) => setTimeout(resolve, ms));
},
// TODO (mirande): replace w/ @particle/async-utils
asyncReduceSeries(array, fn, initial){
return array.reduce((promise, current, index, source) => {
return promise.then((result) => fn(result, current, index, source));
}, Promise.resolve(initial));
},
// TODO (mirande): replace w/ @particle/async-utils
asyncMapSeries(array, fn){
const { asyncReduceSeries } = module.exports;
return asyncReduceSeries(array, async (result, current, index, source) => {
const value = await fn(current, index, source);
result.push(value);
return result;
}, []);
},
// TODO (mirande): replace w/ @particle/async-utils
enforceTimeout(promise, ms){
const delay = new Promise((resolve) => setTimeout(resolve, ms).unref());
const timer = delay.then(() => {
const error = new Error('The operation timed out :(');
error.isTimeout = true;
throw error;
});
return Promise.race([promise, timer]);
},
async retryDeferred(testFn, numTries, recoveryFn){
if (!testFn){
console.error('retryDeferred - comon, pass me a real function.');
return Promise.reject('not a function!');
}
return new Promise((resolve, reject) => {
let lastError = null;
const tryTestFn = (async () => {
numTries--;
if (numTries < 0){
return reject(lastError);
}
try {
const value = await Promise.resolve(testFn());
return resolve(value);
} catch (error){
lastError = error;
if (typeof recoveryFn === 'function'){
Promise.resolve(recoveryFn()).then(tryTestFn);
} else {
tryTestFn();
}
}
})();
});
},
globList(basepath, arr, { followSymlinks } = {}){
let line, found, files = [];
for (let i = 0;i < arr.length;i++){
line = arr[i];
if (basepath){
line = path.join(basepath, line);
}
found = glob.sync(line, { nodir: true, follow: !!followSymlinks });
if (os.platform() === 'win32') {
// because glob pattern is always in posix format
found = found.map(file => path.win32.normalize(file));
}
if (found && (found.length > 0)){
files = files.concat(found);
}
}
return files;
},
trimBlankLinesAndComments(arr){
if (arr && (arr.length !== 0)){
return arr.filter((obj) => {
return obj && (obj !== '') && (obj.indexOf('#') !== 0);
});
}
return arr;
},
readAndTrimLines(file){
if (!fs.existsSync(file)){
return null;
}
const str = fs.readFileSync(file).toString();
if (!str){
return null;
}
const arr = str.split('\n');
if (arr && (arr.length > 0)){
for (let i = 0; i < arr.length; i++){
arr[i] = arr[i].trim();
}
}
return arr;
},
arrayToHashSet(arr){
const h = {};
if (arr) {
for (let i = 0; i < arr.length; i++) {
h[arr[i]] = true;
}
}
return h;
},
tryParse(str){
try {
if (str){
return JSON.parse(str);
}
} catch (ex){
console.error('tryParse error ', ex);
}
},
/**
* replace unfriendly resolution / rejected messages with something nice.
*
* @param {Promise} promise
* @param {*} res
* @param {*} err
* @returns {Promise} promise, resolving with res, or rejecting with err
*/
replaceDfdResults(promise, res, err){
return Promise.resolve(promise)
.then(() => res)
.catch(() => err);
},
replaceAll(str, src, dest) {
return str.split(src).join(dest);
},
compliment(arr, excluded){
const { arrayToHashSet } = module.exports;
const hash = arrayToHashSet(excluded);
const result = [];
for (let i = 0;i < arr.length;i++){
const key = arr[i];
if (!hash[key]){
result.push(key);
}
}
return result;
},
tryDelete(filename){
try {
if (fs.existsSync(filename)){
fs.unlinkSync(filename);
}
return true;
} catch (_err){
console.error('error deleting file ' + filename);
}
return false;
},
__banner: undefined,
banner(){
const bannerFile = path.join(__dirname, '../../assets/banner.txt');
if (module.exports.__banner === undefined){
try {
module.exports.__banner = fs.readFileSync(bannerFile, 'utf8');
} catch (_err){
// ignore missing banner
}
}
return module.exports.__banner;
},
// generates an object like { photon: 6, electron: 10 }
knownPlatformIds(){
return PLATFORMS.reduce((platforms, platform) => {
platforms[platform.name] = platform.id;
return platforms;
}, {});
},
knownPlatformIdsWithAliases(predicate = () => true){
return PLATFORMS.reduce((platforms, platform) => {
if (!predicate(platform)) {
return platforms;
}
platforms[platform.name] = platform.id;
(platform.aliases || []).reduce((platforms, alias) => {
platforms[alias] = platform.id;
return platforms;
}, platforms);
return platforms;
}, {});
},
// generates an object like { 6: 'Photon', 10: 'Electron' }
knownPlatformDisplayForId(){
return PLATFORMS.reduce((platforms, platform) => {
platforms[platform.id] = platform.displayName;
return platforms;
}, {});
},
/**
* Generates a filter function to be used with `Array.filter()` when filtering a list of Devices
* by some value. Supports `online`, `offline`, Platform Name, Device ID, or Device Name
*
* @param {string} filter - Filter value to use for filtering a list of devices
* @returns {function|null}
*/
buildDeviceFilter(filter) {
const { knownPlatformIds } = module.exports;
let filterFunc = null;
if (filter){
const platforms = knownPlatformIds();
if (filter === 'online') {
filterFunc = (d) => d.connected;
} else if (filter === 'offline') {
filterFunc = (d) => !d.connected;
} else if (Object.keys(platforms).indexOf(filter) >= 0) {
filterFunc = (d) => d.platform_id === platforms[filter];
} else {
filterFunc = (d) => d.id === filter || d.name === filter;
}
}
return filterFunc;
},
ensureError(err){
if (err instanceof DeviceProtectionError) {
return new Error('Operation could not be completed due to Device Protection');
}
if (!_.isError(err) && !(err instanceof VError)){
return new Error(_.isArray(err) ? err.join('\n') : err);
}
return err;
},
parsePropertyFile(propPath) {
const savedProp = fs.readFileSync(propPath, 'utf8');
const parsedPropFile = propertiesParser.parse(savedProp);
return parsedPropFile;
},
/**
* Converts a string to the slug version by replacing
* spaces and underscores with dashes, changing
* to lowercase, and removing anything other than
* numbers, letters, and dashes
* @param {String} str string to slugify
* @return {String} slugified version of str
*/
slugify(str) {
const slug = str.trim().toLowerCase()
// replace every group of spaces and underscores with a single hyphen
.replace(/[ _]+/g, '-')
// delete everything other than letters, numbers and hyphens
.replace(/[^a-z0-9-]/g, '');
return slug;
},
/**
* Returns the architecture of the current system
* @return {String} architecture of the current system
* @example 'x64', 'x86-64', etc
*/
getArchType(){
return os.arch();
},
/**
* Returns the os of the current system
* @return {String} os of the current system
* @example 'linux', 'darwin', etc
*/
getOs(){
return os.platform();
},
/**
*
* @param filePath
* @return {Promise<unknown>}
*/
sha256File(filePath) {
return new Promise((resolve, reject) => {
const hash = createHash('sha256');
const stream = fs.createReadStream(filePath);
stream.on('data', (chunk) => hash.update(chunk));
stream.on('end', () => resolve(hash.digest('hex')));
stream.on('error', reject);
});
},
/**
*
*/
async fileExists(filePath){
try {
await fs.access(filePath);
return true;
} catch (_err) {
return false;
}
},
/**
*
*/
compressDir({ pattern, pathToCompress, outputDir, outputFile }) {
const outputFilePath = path.join(outputDir, outputFile);
const output = fs.createWriteStream(outputFilePath);
const archive = archiver('zip', {
zlib: { level: 9 } // Sets the compression level.
});
archive.pipe(output);
if (pattern) {
archive.glob(pattern, { cwd: pathToCompress, date: new Date(0) });
} else {
archive.directory(pathToCompress, false, {
date: new Date(0),
mode: 0o100644,
store: false
});
}
archive.finalize();
return new Promise((resolve, reject) => {
output.on('close', async () => {
const sha256 = await module.exports.sha256File(outputFilePath);
return resolve({
totalBytes: archive.pointer(),
zipPath: outputFilePath,
outputFile: outputFile,
sha256
});
});
output.on('end', () => {
console.log('Data has been drained');
});
archive.on('warning', (err) => {
if (err.code === 'ENOENT') {
// log warning
console.log('file not found', err);
} else {
// throw error
return reject(err);
}
});
archive.on('error', (err) => {
return reject(err);
});
});
},
};