UNPKG

11.1 kBJavaScriptView Raw
1'use strict';
2
3Object.defineProperty(exports, "__esModule", {
4 value: true
5});
6exports.Updater = undefined;
7
8var _path = require('path');
9
10var _path2 = _interopRequireDefault(_path);
11
12var _fsExtra = require('fs-extra');
13
14var _fsExtra2 = _interopRequireDefault(_fsExtra);
15
16require('cli-engine-config');
17
18var _cliUx = require('cli-ux');
19
20function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
21
22const deps = {
23 get Lock() {
24 return this._lock || (this._lock = require('./lock').default);
25 },
26 get HTTP() {
27 return this._http || (this._http = require('http-call').default);
28 },
29 get moment() {
30 return this._moment || (this._moment = require('moment'));
31 },
32 get util() {
33 return this._util || (this._util = require('./util'));
34 },
35 get wait() {
36 return this.util.wait;
37 }
38};
39
40const debug = require('debug')('cli:updater');
41
42function mtime(f) {
43 return deps.moment(_fsExtra2.default.statSync(f).mtime);
44}
45
46function timestamp(msg) {
47 return `[${deps.moment().format()}] ${msg}`;
48}
49
50class Updater {
51
52 constructor(config, cli) {
53 this.config = config;
54 this.cli = cli || new _cliUx.CLI({ mock: config.mock });
55 this.lock = new deps.Lock(config);
56 }
57
58 get autoupdatefile() {
59 return _path2.default.join(this.config.cacheDir, 'autoupdate');
60 }
61 get autoupdatelogfile() {
62 return _path2.default.join(this.config.cacheDir, 'autoupdate.log');
63 }
64 get binPath() {
65 return process.env.CLI_BINPATH || this.config.bin;
66 }
67 get updateDir() {
68 return _path2.default.join(this.config.dataDir, 'tmp', 'u');
69 }
70 get versionFile() {
71 return _path2.default.join(this.config.cacheDir, `${this.config.channel}.version`);
72 }
73
74 s3url(channel, p) {
75 if (!this.config.s3.host) throw new Error('S3 host not defined');
76 if (/^https?:\/\/.*/.test(this.config.s3.host)) {
77 return `${this.config.s3.host}/${this.config.name}/channels/${channel}/${p}`;
78 } else {
79 return `https://${this.config.s3.host}/${this.config.name}/channels/${channel}/${p}`;
80 }
81 }
82
83 async fetchManifest(channel) {
84 try {
85 let { body } = await deps.HTTP.get(this.s3url(channel, `${this.config.platform}-${this.config.arch}`));
86 return body;
87 } catch (err) {
88 if (err.statusCode === 403) throw new Error(`HTTP 403: Invalid channel ${channel}`);
89 throw err;
90 }
91 }
92
93 async fetchVersion(download) {
94 let v;
95 try {
96 if (!download) v = await _fsExtra2.default.readJSON(this.versionFile);
97 } catch (err) {
98 if (err.code !== 'ENOENT') throw err;
99 }
100 if (!v) {
101 debug('fetching latest %s version', this.config.channel);
102 let { body } = await deps.HTTP.get(this.s3url(this.config.channel, 'version'));
103 v = body;
104 await this._catch(() => _fsExtra2.default.writeJSON(this.versionFile, v));
105 }
106 return v;
107 }
108
109 async _catch(fn) {
110 try {
111 return await Promise.resolve(fn());
112 } catch (err) {
113 debug(err);
114 }
115 }
116
117 async update(manifest) {
118 let base = this.base(manifest);
119 const filesize = require('filesize');
120
121 let url = this.s3url(manifest.channel, `${base}.tar.gz`);
122 let { response: stream } = await deps.HTTP.stream(url);
123
124 if (this.cli.action.frames) {
125 // if spinner action
126 let total = stream.headers['content-length'];
127 let current = 0;
128 const throttle = require('lodash.throttle');
129 const updateStatus = throttle(newStatus => {
130 this.cli.action.status = newStatus;
131 }, 500, { leading: true, trailing: false });
132 stream.on('data', data => {
133 current += data.length;
134 updateStatus(`${filesize(current)}/${filesize(total)}`);
135 });
136 }
137
138 _fsExtra2.default.mkdirpSync(this.updateDir);
139 let dirs = this._dirs(require('tmp').dirSync({ dir: this.updateDir }).name);
140
141 let dir = _path2.default.join(this.config.dataDir, 'client');
142 let tmp = dirs.extract;
143
144 await this.extract(stream, tmp, manifest.sha256gz);
145 let extracted = _path2.default.join(dirs.extract, base);
146
147 this._cleanup();
148
149 this.cli.action.status = 'finishing up';
150 let downgrade = await this.lock.upgrade();
151 // wait 2000ms for any commands that were partially loaded to finish loading
152 await deps.wait(2000);
153 if (await _fsExtra2.default.exists(dir)) this._rename(dir, dirs.client);
154 this._rename(extracted, dir);
155 downgrade();
156
157 this._cleanupDirs(dirs);
158 }
159
160 extract(stream, dir, sha) {
161 const zlib = require('zlib');
162 const tar = require('tar-fs');
163 const crypto = require('crypto');
164
165 return new Promise((resolve, reject) => {
166 let shaValidated = false;
167 let extracted = false;
168
169 let check = () => {
170 if (shaValidated && extracted) {
171 resolve();
172 }
173 };
174
175 let fail = err => {
176 this._catch(() => {
177 if (_fsExtra2.default.existsSync(dir)) {
178 _fsExtra2.default.removeSync(dir);
179 }
180 });
181 reject(err);
182 };
183
184 let hasher = crypto.createHash('sha256');
185 stream.on('error', fail);
186 stream.on('data', d => hasher.update(d));
187 stream.on('end', () => {
188 let shasum = hasher.digest('hex');
189 if (sha === shasum) {
190 shaValidated = true;
191 check();
192 } else {
193 reject(new Error(`SHA mismatch: expected ${shasum} to be ${sha}`));
194 }
195 });
196
197 let ignore = function (_, header) {
198 switch (header.type) {
199 case 'directory':
200 case 'file':
201 return false;
202 case 'symlink':
203 return true;
204 default:
205 throw new Error(header.type);
206 }
207 };
208 let extract = tar.extract(dir, { ignore });
209 extract.on('error', fail);
210 extract.on('finish', () => {
211 extracted = true;
212 check();
213 });
214
215 let gunzip = zlib.createGunzip();
216 gunzip.on('error', fail);
217
218 stream.pipe(gunzip).pipe(extract);
219 });
220 }
221
222 _cleanup() {
223 let dir = this.updateDir;
224 this._catch(() => {
225 if (_fsExtra2.default.existsSync(dir)) {
226 _fsExtra2.default.readdirSync(dir).forEach(d => {
227 let dirs = this._dirs(_path2.default.join(dir, d));
228
229 this._remove(dirs.node);
230
231 if (mtime(dirs.dir).isBefore(deps.moment().subtract(24, 'hours'))) {
232 this._cleanupDirs(dirs);
233 } else {
234 this._removeIfEmpty(dirs);
235 }
236 });
237 }
238 });
239 }
240
241 _cleanupDirs(dirs) {
242 this._moveNode(dirs);
243
244 this._remove(dirs.client);
245 this._remove(dirs.extract);
246 this._removeIfEmpty(dirs);
247 }
248
249 _removeIfEmpty(dirs) {
250 this._catch(() => {
251 if (_fsExtra2.default.readdirSync(dirs.dir).length === 0) {
252 this._remove(dirs.dir);
253 }
254 });
255 }
256
257 _dirs(dir) {
258 let client = _path2.default.join(dir, 'client');
259 let extract = _path2.default.join(dir, 'extract');
260 let node = _path2.default.join(dir, 'node.exe');
261
262 return { dir, client, extract, node };
263 }
264
265 _rename(src, dst) {
266 debug(`rename ${src} to ${dst}`);
267 // moveSync tries to do a rename first then falls back to copy & delete
268 // on windows the delete would error on node.exe so we explicitly rename
269 let rename = this.config.windows ? _fsExtra2.default.renameSync : _fsExtra2.default.moveSync;
270 rename(src, dst);
271 }
272
273 _remove(dir) {
274 this._catch(() => {
275 if (_fsExtra2.default.existsSync(dir)) {
276 debug(`remove ${dir}`);
277 _fsExtra2.default.removeSync(dir);
278 }
279 });
280 }
281
282 _moveNode(dirs) {
283 this._catch(() => {
284 let dirDeleteNode = _path2.default.join(dirs.client, 'bin', 'node.exe');
285 if (_fsExtra2.default.existsSync(dirDeleteNode)) {
286 this._rename(dirDeleteNode, dirs.node);
287 }
288 });
289 }
290
291 base(manifest) {
292 return `${this.config.name}-v${manifest.version}-${this.config.platform}-${this.config.arch}`;
293 }
294
295 get autoupdateNeeded() {
296 try {
297 return mtime(this.autoupdatefile).isBefore(deps.moment().subtract(5, 'hours'));
298 } catch (err) {
299 if (err.code !== 'ENOENT') console.error(err.stack);
300 debug(err);
301 return true;
302 }
303 }
304
305 async autoupdate(force = false) {
306 try {
307 await this.checkIfUpdating();
308 await this.warnIfUpdateAvailable();
309 if (!force && !this.autoupdateNeeded) return;
310
311 debug('autoupdate running');
312 _fsExtra2.default.outputFileSync(this.autoupdatefile, '');
313
314 const binPath = this.binPath;
315 if (!binPath) {
316 debug('no binpath set');
317 return;
318 }
319 debug(`spawning autoupdate on ${binPath}`);
320
321 let fd = _fsExtra2.default.openSync(this.autoupdatelogfile, 'a');
322 _fsExtra2.default.write(fd, timestamp(`starting \`${binPath} update --autoupdate\` from ${process.argv.slice(2, 3).join(' ')}\n`));
323
324 const { spawn } = require('child_process');
325 this.spawnBinPath(spawn, binPath, ['update', '--autoupdate'], {
326 detached: !this.config.windows,
327 stdio: ['ignore', fd, fd],
328 env: this.autoupdateEnv
329 }).on('error', e => this.cli.warn(e, 'autoupdate:')).unref();
330 } catch (e) {
331 this.cli.warn(e, 'autoupdate:');
332 }
333 }
334
335 get timestampEnvVar() {
336 // TODO: use function from cli-engine-config
337 let bin = this.config.bin.replace('-', '_').toUpperCase();
338 return `${bin}_TIMESTAMPS`;
339 }
340
341 get autoupdateEnv() {
342 return Object.assign({}, process.env, {
343 [this.timestampEnvVar]: '1'
344 });
345 }
346
347 async warnIfUpdateAvailable() {
348 await this._catch(async () => {
349 if (!this.config.s3) return;
350 let v = await this.fetchVersion(false);
351 let local = this.config.version.split('.');
352 let remote = v.version.split('.');
353 if (parseInt(local[0]) < parseInt(remote[0]) || parseInt(local[1]) < parseInt(remote[1])) {
354 this.cli.warn(`${this.config.name}: update available from ${this.config.version} to ${v.version}`);
355 }
356 if (v.message) {
357 this.cli.warn(`${this.config.name}: ${v.message}`);
358 }
359 });
360 }
361
362 async checkIfUpdating() {
363 if (!(await this.lock.canRead())) {
364 debug('update in process');
365 await this.restartCLI();
366 } else await this.lock.read();
367 }
368
369 async restartCLI() {
370 let unread = await this.lock.read();
371 await unread();
372
373 const { spawnSync } = require('child_process');
374 let bin = this.binPath;
375 let args = process.argv.slice(2);
376 if (!bin) {
377 if (this.config.initPath) {
378 bin = process.argv[0];
379 args.unshift(this.config.initPath);
380 } else {
381 debug('cannot restart CLI, no binpath');
382 return;
383 }
384 }
385
386 debug('update complete, restarting CLI');
387 const env = {
388 ...process.env,
389 CLI_ENGINE_HIDE_UPDATED_MESSAGE: '1'
390 };
391 const { status } = this.spawnBinPath(spawnSync, bin, args, { env, stdio: 'inherit' });
392 this.cli.exit(status);
393 }
394
395 spawnBinPath(spawnFunc, binPath, args, options) {
396 if (this.config.windows) {
397 args = ['/c', binPath].concat(args);
398 return spawnFunc(process.env.comspec || 'cmd.exe', args, options);
399 } else {
400 return spawnFunc(binPath, args, options);
401 }
402 }
403}
404exports.Updater = Updater;
\No newline at end of file