UNPKG

13.3 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3const tslib_1 = require("tslib");
4const cli_ux_1 = require("cli-ux");
5const path = require("path");
6const rwlockfile_1 = require("rwlockfile");
7const ts_lodash_1 = require("ts-lodash");
8const deps_1 = require("./deps");
9const { spawn } = require('cross-spawn');
10const debug = require('debug')('cli:updater');
11function mtime(f) {
12 return tslib_1.__awaiter(this, void 0, void 0, function* () {
13 const { mtime } = yield deps_1.default.file.stat(f);
14 return deps_1.default.moment(mtime);
15 });
16}
17function timestamp(msg) {
18 return `[${deps_1.default.moment().format()}] ${msg}`;
19}
20class Updater {
21 constructor(config) {
22 this.config = config;
23 this.http = deps_1.default.HTTP.defaults({ headers: { 'user-agent': config.userAgent } });
24 }
25 get autoupdatefile() {
26 return path.join(this.config.cacheDir, 'autoupdate');
27 }
28 get autoupdatelogfile() {
29 return path.join(this.config.cacheDir, 'autoupdate.log');
30 }
31 get versionFile() {
32 return path.join(this.config.cacheDir, `${this.config.channel}.version`);
33 }
34 get clientRoot() {
35 return path.join(this.config.dataDir, 'client');
36 }
37 get clientBin() {
38 let b = path.join(this.clientRoot, 'bin', this.config.bin);
39 return this.config.windows ? `${b}.cmd` : b;
40 }
41 get binPath() {
42 return this.config.reexecBin || this.config.bin;
43 }
44 get s3Host() {
45 return this.config.s3 && this.config.s3.host;
46 }
47 s3url(channel, p) {
48 if (!this.s3Host)
49 throw new Error('S3 host not defined');
50 return `https://${this.s3Host}/${this.config.name}/channels/${channel}/${p}`;
51 }
52 fetchManifest(channel) {
53 return tslib_1.__awaiter(this, void 0, void 0, function* () {
54 try {
55 let { body } = yield this.http.get(this.s3url(channel, `${this.config.platform}-${this.config.arch}`));
56 return body;
57 }
58 catch (err) {
59 if (err.statusCode === 403)
60 throw new Error(`HTTP 403: Invalid channel ${channel}`);
61 throw err;
62 }
63 });
64 }
65 fetchVersion(download) {
66 return tslib_1.__awaiter(this, void 0, void 0, function* () {
67 let v;
68 try {
69 if (!download)
70 v = yield deps_1.default.file.readJSON(this.versionFile);
71 }
72 catch (err) {
73 if (err.code !== 'ENOENT')
74 throw err;
75 }
76 if (!v) {
77 debug('fetching latest %s version', this.config.channel);
78 let { body } = yield this.http.get(this.s3url(this.config.channel, 'version'));
79 v = body;
80 yield this._catch(() => deps_1.default.file.outputJSON(this.versionFile, v));
81 }
82 return v;
83 });
84 }
85 warnIfUpdateAvailable() {
86 return tslib_1.__awaiter(this, void 0, void 0, function* () {
87 yield this._catch(() => tslib_1.__awaiter(this, void 0, void 0, function* () {
88 if (!this.s3Host)
89 return;
90 let v = yield this.fetchVersion(false);
91 if (deps_1.default.util.minorVersionGreater(this.config.version, v.version)) {
92 cli_ux_1.cli.warn(`${this.config.name}: update available from ${this.config.version} to ${v.version}`);
93 }
94 if (v.message) {
95 cli_ux_1.cli.warn(`${this.config.name}: ${v.message}`);
96 }
97 }));
98 });
99 }
100 autoupdate(force = false) {
101 return tslib_1.__awaiter(this, void 0, void 0, function* () {
102 try {
103 const clientDir = path.join(this.clientRoot, this.config.version);
104 if (yield deps_1.default.file.exists(clientDir)) {
105 yield deps_1.default.file.touch(clientDir);
106 }
107 yield this.warnIfUpdateAvailable();
108 if (!force && !(yield this.autoupdateNeeded()))
109 return;
110 debug('autoupdate running');
111 debug(`spawning autoupdate on ${this.binPath}`);
112 let fd = yield deps_1.default.file.open(this.autoupdatelogfile, 'a');
113 deps_1.default.file.write(fd, timestamp(`starting \`${this.binPath} update --autoupdate\` from ${process.argv.slice(1, 3).join(' ')}\n`));
114 spawn(this.binPath, ['update', '--autoupdate'], {
115 detached: !this.config.windows,
116 stdio: ['ignore', fd, fd],
117 env: this.autoupdateEnv,
118 })
119 .on('error', (e) => cli_ux_1.cli.warn(e, { context: 'autoupdate:' }))
120 .unref();
121 }
122 catch (e) {
123 cli_ux_1.cli.warn(e, { context: 'autoupdate:' });
124 }
125 finally {
126 yield deps_1.default.file.touch(this.autoupdatefile);
127 }
128 });
129 }
130 update(manifest) {
131 return tslib_1.__awaiter(this, void 0, void 0, function* () {
132 let base = this.base(manifest);
133 const output = path.join(this.clientRoot, manifest.version);
134 const tmp = path.join(this.clientRoot, base);
135 const lock = new rwlockfile_1.default(this.autoupdatefile, { ifLocked: () => cli_ux_1.cli.action.start('CLI is updating') });
136 if (!this.s3Host)
137 throw new Error('S3 host not defined');
138 yield lock.add('write', { reason: 'update' });
139 try {
140 let url = `https://${this.s3Host}/${this.config.name}/channels/${manifest.channel}/${base}.tar.gz`;
141 let { response: stream } = yield this.http.stream(url);
142 yield deps_1.default.file.emptyDir(tmp);
143 let extraction = this.extract(stream, this.clientRoot, manifest.sha256gz);
144 // TODO: use cli.action.type
145 if (deps_1.default.filesize && cli_ux_1.cli.action.frames) {
146 // if spinner action
147 let total = stream.headers['content-length'];
148 let current = 0;
149 const updateStatus = ts_lodash_1.default.throttle((newStatus) => {
150 cli_ux_1.cli.action.status = newStatus;
151 }, 500, { leading: true, trailing: false });
152 stream.on('data', data => {
153 current += data.length;
154 updateStatus(`${deps_1.default.filesize(current)}/${deps_1.default.filesize(total)}`);
155 });
156 }
157 yield extraction;
158 if (yield deps_1.default.file.exists(output)) {
159 const old = `${output}.old`;
160 yield deps_1.default.file.remove(old);
161 yield deps_1.default.file.rename(output, old);
162 }
163 yield deps_1.default.file.rename(tmp, output);
164 yield deps_1.default.file.touch(output);
165 yield this._createBin(manifest);
166 }
167 finally {
168 yield lock.remove('write');
169 }
170 yield this.reexecUpdate();
171 });
172 }
173 tidy() {
174 return tslib_1.__awaiter(this, void 0, void 0, function* () {
175 try {
176 if (!this.config.reexecBin)
177 return;
178 if (!this.config.reexecBin.includes(this.config.version))
179 return;
180 const { moment, file } = deps_1.default;
181 let root = this.clientRoot;
182 if (!(yield file.exists(root)))
183 return;
184 let files = yield file.ls(root);
185 let promises = files.map((f) => tslib_1.__awaiter(this, void 0, void 0, function* () {
186 if (['bin', this.config.version].includes(path.basename(f.path)))
187 return;
188 if (moment(f.stat.mtime).isBefore(moment().subtract(7, 'days'))) {
189 yield file.remove(f.path);
190 }
191 }));
192 for (let p of promises)
193 yield p;
194 }
195 catch (err) {
196 cli_ux_1.cli.warn(err);
197 }
198 });
199 }
200 extract(stream, dir, sha) {
201 const zlib = require('zlib');
202 const tar = require('tar-fs');
203 const crypto = require('crypto');
204 return new Promise((resolve, reject) => {
205 let shaValidated = false;
206 let extracted = false;
207 let check = () => {
208 if (shaValidated && extracted) {
209 resolve();
210 }
211 };
212 let fail = (err) => {
213 deps_1.default.file.remove(dir).then(() => reject(err));
214 };
215 let hasher = crypto.createHash('sha256');
216 stream.on('error', fail);
217 stream.on('data', d => hasher.update(d));
218 stream.on('end', () => {
219 let shasum = hasher.digest('hex');
220 if (sha === shasum) {
221 shaValidated = true;
222 check();
223 }
224 else {
225 reject(new Error(`SHA mismatch: expected ${shasum} to be ${sha}`));
226 }
227 });
228 let ignore = (_, header) => {
229 switch (header.type) {
230 case 'directory':
231 case 'file':
232 if (process.env.CLI_ENGINE_DEBUG_UPDATE_FILES)
233 debug(header.name);
234 return false;
235 case 'symlink':
236 return true;
237 default:
238 throw new Error(header.type);
239 }
240 };
241 let extract = tar.extract(dir, { ignore });
242 extract.on('error', fail);
243 extract.on('finish', () => {
244 extracted = true;
245 check();
246 });
247 let gunzip = zlib.createGunzip();
248 gunzip.on('error', fail);
249 stream.pipe(gunzip).pipe(extract);
250 });
251 }
252 base(manifest) {
253 return `${this.config.name}-v${manifest.version}-${this.config.platform}-${this.config.arch}`;
254 }
255 autoupdateNeeded() {
256 return tslib_1.__awaiter(this, void 0, void 0, function* () {
257 try {
258 const m = yield mtime(this.autoupdatefile);
259 return m.isBefore(deps_1.default.moment().subtract(5, 'hours'));
260 }
261 catch (err) {
262 if (err.code !== 'ENOENT')
263 cli_ux_1.cli.error(err.stack);
264 if (global.testing)
265 return false;
266 debug('autoupdate ENOENT');
267 return true;
268 }
269 });
270 }
271 get timestampEnvVar() {
272 // TODO: use function from @cli-engine/config
273 let bin = this.config.bin.replace('-', '_').toUpperCase();
274 return `${bin}_TIMESTAMPS`;
275 }
276 get skipAnalyticsEnvVar() {
277 let bin = this.config.bin.replace('-', '_').toUpperCase();
278 return `${bin}_SKIP_ANALYTICS`;
279 }
280 get autoupdateEnv() {
281 return Object.assign({}, process.env, {
282 [this.timestampEnvVar]: '1',
283 [this.skipAnalyticsEnvVar]: '1',
284 });
285 }
286 reexecUpdate() {
287 return tslib_1.__awaiter(this, void 0, void 0, function* () {
288 cli_ux_1.cli.action.stop();
289 return new Promise((_, reject) => {
290 debug('restarting CLI after update', this.clientBin);
291 spawn(this.clientBin, ['update'], {
292 stdio: 'inherit',
293 env: Object.assign({}, process.env, { CLI_ENGINE_HIDE_UPDATED_MESSAGE: '1' }),
294 })
295 .on('error', reject)
296 .on('close', (status) => {
297 try {
298 cli_ux_1.cli.exit(status);
299 }
300 catch (err) {
301 reject(err);
302 }
303 });
304 });
305 });
306 }
307 _createBin(manifest) {
308 return tslib_1.__awaiter(this, void 0, void 0, function* () {
309 let dst = this.clientBin;
310 if (this.config.windows) {
311 let body = `@echo off
312"%~dp0\\..\\${manifest.version}\\bin\\${this.config.bin}.cmd" %*
313`;
314 yield deps_1.default.file.outputFile(dst, body);
315 return;
316 }
317 let src = path.join('..', manifest.version, 'bin', this.config.bin);
318 yield deps_1.default.file.mkdirp(path.dirname(dst));
319 yield deps_1.default.file.remove(dst);
320 yield deps_1.default.file.symlink(src, dst);
321 });
322 }
323 _catch(fn) {
324 return tslib_1.__awaiter(this, void 0, void 0, function* () {
325 try {
326 return yield Promise.resolve(fn());
327 }
328 catch (err) {
329 debug(err);
330 }
331 });
332 }
333}
334exports.Updater = Updater;