UNPKG

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
'use strict'; 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); }); }); }, };