UNPKG

cli-engine

Version:
378 lines (310 loc) 10.4 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); require('cli-engine-config'); var _http = require('cli-engine-command/lib/http'); var _http2 = _interopRequireDefault(_http); var _path = require('path'); var _path2 = _interopRequireDefault(_path); var _lock = require('./lock'); var _lock2 = _interopRequireDefault(_lock); var _fsExtra = require('fs-extra'); var _fsExtra2 = _interopRequireDefault(_fsExtra); var _moment = require('moment'); var _moment2 = _interopRequireDefault(_moment); var _util = require('./util'); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } const debug = require('debug')('cli-engine:updater'); function mtime(f) { return (0, _moment2.default)(_fsExtra2.default.statSync(f).mtime); } function timestamp(msg) { return `[${(0, _moment2.default)().format()}] ${msg}`; } class Updater { constructor(output) { this.out = output; this.config = output.config; this.http = new _http2.default(output); this.lock = new _lock2.default(output); } get autoupdatefile() { return _path2.default.join(this.config.cacheDir, 'autoupdate'); } get autoupdatelogfile() { return _path2.default.join(this.config.cacheDir, 'autoupdate.log'); } get binPath() { return process.env.CLI_BINPATH; } get updateDir() { return _path2.default.join(this.config.dataDir, 'tmp', 'u'); } get versionFile() { return _path2.default.join(this.config.cacheDir, `${this.config.channel}.version`); } s3url(channel, p) { if (!this.config.s3.host) throw new Error('S3 host not defined'); return `https://${this.config.s3.host}/${this.config.name}/channels/${channel}/${p}`; } async fetchManifest(channel) { try { return await this.http.get(this.s3url(channel, `${this.config.platform}-${this.config.arch}`)); } catch (err) { if (err.statusCode === 403) throw new Error(`HTTP 403: Invalid channel ${channel}`); throw err; } } async fetchVersion(channel, download) { let v; try { if (!download) v = await _fsExtra2.default.readJSON(this.versionFile); } catch (err) { if (err.code !== 'ENOENT') throw err; } if (!v) { v = await this.http.get(this.s3url(channel, 'version')); await this._catch(() => _fsExtra2.default.writeJSON(this.versionFile, v)); } return v; } _catch(fn) { try { return Promise.resolve(fn()); } catch (err) { this.out.debug(err); } } async update(manifest) { let base = this.base(manifest); const filesize = require('filesize'); if (!this.config.s3.host) throw new Error('S3 host not defined'); let url = `https://${this.config.s3.host}/${this.config.name}/channels/${manifest.channel}/${base}.tar.gz`; let stream = await this.http.stream(url); if (this.out.action.frames) { // if spinner action let total = stream.headers['content-length']; let current = 0; stream.on('data', data => { current += data.length; this.out.action.status = `${filesize(current)}/${filesize(total)}`; }); } _fsExtra2.default.mkdirpSync(this.updateDir); let dirs = this._dirs(require('tmp').dirSync({ dir: this.updateDir }).name); let dir = _path2.default.join(this.config.dataDir, 'client'); let tmp = dirs.extract; await this.extract(stream, tmp, manifest.sha256gz); let extracted = _path2.default.join(dirs.extract, base); this._cleanup(); let downgrade = await this.lock.upgrade(); // wait 1000ms for any commands that were partially loaded to finish loading await (0, _util.wait)(1000); if (await _fsExtra2.default.exists(dir)) this._rename(dir, dirs.client); this._rename(extracted, dir); downgrade(); this._cleanupDirs(dirs); } extract(stream, dir, sha) { const zlib = require('zlib'); const tar = require('tar-fs'); const crypto = require('crypto'); return new Promise((resolve, reject) => { let shaValidated = false; let extracted = false; let check = () => { if (shaValidated && extracted) { resolve(); } }; let fail = err => { this._catch(() => { if (_fsExtra2.default.existsSync(dir)) { _fsExtra2.default.removeSync(dir); } }); reject(err); }; let hasher = crypto.createHash('sha256'); stream.on('error', fail); stream.on('data', d => hasher.update(d)); stream.on('end', () => { let shasum = hasher.digest('hex'); if (sha === shasum) { shaValidated = true; check(); } else { reject(new Error(`SHA mismatch: expected ${shasum} to be ${sha}`)); } }); let ignore = function (_, header) { switch (header.type) { case 'directory': case 'file': return false; case 'symlink': return true; default: throw new Error(header.type); } }; let extract = tar.extract(dir, { ignore }); extract.on('error', fail); extract.on('finish', () => { extracted = true; check(); }); let gunzip = zlib.createGunzip(); gunzip.on('error', fail); stream.pipe(gunzip).pipe(extract); }); } _cleanup() { let dir = this.updateDir; this._catch(() => { if (_fsExtra2.default.existsSync(dir)) { _fsExtra2.default.readdirSync(dir).forEach(d => { let dirs = this._dirs(_path2.default.join(dir, d)); this._remove(dirs.node); if (mtime(dirs.dir).isBefore((0, _moment2.default)().subtract(24, 'hours'))) { this._cleanupDirs(dirs); } else { this._removeIfEmpty(dirs); } }); } }); } _cleanupDirs(dirs) { this._moveNode(dirs); this._remove(dirs.client); this._remove(dirs.extract); this._removeIfEmpty(dirs); } _removeIfEmpty(dirs) { this._catch(() => { if (_fsExtra2.default.readdirSync(dirs.dir).length === 0) { this._remove(dirs.dir); } }); } _dirs(dir) { let client = _path2.default.join(dir, 'client'); let extract = _path2.default.join(dir, 'extract'); let node = _path2.default.join(dir, 'node.exe'); return { dir, client, extract, node }; } _rename(src, dst) { this.out.debug(`rename ${src} to ${dst}`); // moveSync tries to do a rename first then falls back to copy & delete // on windows the delete would error on node.exe so we explicitly rename let rename = this.config.windows ? _fsExtra2.default.renameSync : _fsExtra2.default.moveSync; rename(src, dst); } _remove(dir) { this._catch(() => { if (_fsExtra2.default.existsSync(dir)) { this.out.debug(`remove ${dir}`); _fsExtra2.default.removeSync(dir); } }); } _moveNode(dirs) { this._catch(() => { let dirDeleteNode = _path2.default.join(dirs.client, 'bin', 'node.exe'); if (_fsExtra2.default.existsSync(dirDeleteNode)) { this._rename(dirDeleteNode, dirs.node); } }); } base(manifest) { return `${this.config.name}-v${manifest.version}-${this.config.platform}-${this.config.arch}`; } get autoupdateNeeded() { try { return mtime(this.autoupdatefile).isBefore((0, _moment2.default)().subtract(5, 'hours')); } catch (err) { if (err.code !== 'ENOENT') console.error(err.stack); return true; } } async autoupdate(force = false) { try { await this.checkIfUpdating(); await this.warnIfUpdateAvailable(); if (!force && !this.autoupdateNeeded) return; debug('autoupdate running'); _fsExtra2.default.outputFileSync(this.autoupdatefile, ''); const binPath = this.binPath; if (!binPath) { debug('no binpath set'); return; } debug(`spawning autoupdate on ${binPath}`); let fd = _fsExtra2.default.openSync(this.autoupdatelogfile, 'a'); _fsExtra2.default.write(fd, timestamp(`starting \`${binPath} update --autoupdate\` from ${process.argv.slice(2, 3).join(' ')}\n`)); const { spawn } = require('child_process'); this.spawnBinPath(spawn, binPath, ['update', '--autoupdate'], { detached: !this.config.windows, stdio: ['ignore', fd, fd], env: this.autoupdateEnv }).on('error', e => this.out.warn(e, 'autoupdate:')).unref(); } catch (e) { this.out.warn(e, 'autoupdate:'); } } get timestampEnvVar() { // TODO: use function from cli-engine-config let bin = this.config.bin.replace('-', '_').toUpperCase(); return `${bin}_TIMESTAMPS`; } get autoupdateEnv() { return Object.assign({}, process.env, { [this.timestampEnvVar]: '1' }); } async warnIfUpdateAvailable() { await this._catch(async () => { let v = await this.fetchVersion(this.config.channel, false); let local = this.config.version.split('.'); let remote = v.version.split('.'); if (parseInt(local[0]) < parseInt(remote[0]) || parseInt(local[1]) < parseInt(remote[1])) { this.out.warn(`${this.config.name}: update available from ${this.config.version} to ${v.version}`); } if (v.message) { this.out.warn(`${this.config.name}: ${v.message}`); } }); } async checkIfUpdating() { debug('check if updating'); if (!(await this.lock.canRead())) { debug('update in process'); await this.restartCLI(); } else await this.lock.read(); debug('done checking if updating'); } async restartCLI() { let unread = await this.lock.read(); await unread(); const { spawnSync } = require('child_process'); const binPath = this.binPath; if (!binPath) { debug('cannot restart CLI, no binpath'); return; } debug('update complete, restarting CLI'); const { status } = this.spawnBinPath(spawnSync, binPath, process.argv.slice(2), { stdio: 'inherit' }); this.out.exit(status); } spawnBinPath(spawnFunc, binPath, args, options) { if (this.config.windows) { args = ['/c', binPath].concat(args); return spawnFunc(process.env.comspec || 'cmd.exe', args, options); } else { return spawnFunc(binPath, args, options); } } } exports.default = Updater;