UNPKG

9.24 kBJavaScriptView Raw
1// jscs:disable jsDoc
2/**
3 * This code is closed source and Confidential and Proprietary to
4 * Appcelerator, Inc. All Rights Reserved. This code MUST not be
5 * modified, copied or otherwise redistributed without express
6 * written permission of Appcelerator. This file is licensed as
7 * part of the Appcelerator Platform and governed under the terms
8 * of the Appcelerator license agreement.
9 */
10var ProgressBar = require('progress'),
11 chalk = require('chalk'),
12 util = require('./util'),
13 errorlib = require('./error'),
14 fs = require('fs'),
15 os = require('os'),
16 path = require('path'),
17 debug = require('debug')('appc:download'),
18 tmpdir = os.tmpdir(),
19 MAX_RETRIES = util.MAX_RETRIES,
20 pendingRequest;
21
22function download(quiet, force, wantVersion, tmpfile, stream, location, callback, nobanner, retryAttempts) {
23 debug('download called with arguments: %o', arguments);
24 if (!nobanner && !wantVersion) {
25 util.waitMessage('Finding latest version ');
26 }
27 if (!nobanner && wantVersion) {
28 util.waitMessage('Finding version ' + wantVersion + ' ');
29 }
30
31 retryAttempts = retryAttempts || 1;
32 debug('connection attempt %d of %d', retryAttempts, MAX_RETRIES);
33
34 var bar;
35 pendingRequest = util.request(location, function (err, res, _req) {
36 if (err) {
37 debug('error from download was: %o', err);
38 if (err.code === 'ECONNREFUSED' || err.code === 'ECONNRESET') {
39 pendingRequest = null;
40 util.resetLine();
41 if (retryAttempts >= MAX_RETRIES) {
42 return callback(errorlib.createError('com.appcelerator.install.download.server.unavailable'));
43 }
44 // retry again
45 debug('retrying request again, count=%d, delay=%d', retryAttempts, 500 * retryAttempts);
46 return setTimeout(function () {
47 download(quiet, force, wantVersion, tmpfile, stream, location, callback, true, retryAttempts + 1);
48 }, 500 * retryAttempts);
49 } else if (err.name === 'AppCError') {
50 return callback(err);
51 }
52 return callback(errorlib.createError('com.appcelerator.install.download.server.response.error', err.message));
53 }
54 debug('response status code was: %d', res.statusCode);
55 // console.log(res);
56 if (res.statusCode === 301 || res.statusCode === 302) {
57 debug('response status code: %d with headers: %j', res.statusCode, res.headers);
58
59 // handle redirect
60 location = res.headers.location;
61 pendingRequest = null;
62 util.resetLine();
63 return download(quiet, force, wantVersion, tmpfile, stream, location, callback, nobanner, retryAttempts);
64 } else if (res.statusCode === 404) {
65 debug('response status code: %d with headers: %j', res.statusCode, res.headers);
66
67 pendingRequest = null;
68 return callback(errorlib.createError('com.appcelerator.install.download.version.specified.incorrect', wantVersion));
69 } else if (res.statusCode === 200) {
70 debug('response status code: %d with headers: %j', res.statusCode, res.headers);
71
72 var version = res.headers['x-appc-version'] || res.headers['x-amz-meta-version'],
73 shasum = res.headers['x-appc-shasum'] || res.headers['x-amz-meta-shasum'],
74 hash = require('crypto').createHash('sha1');
75
76 hash.setEncoding('hex');
77
78 debug('download version: %s, shasum: %s', version, shasum);
79
80 if (!nobanner && !wantVersion) {
81 util.okMessage(chalk.green(version));
82 }
83 if (!nobanner && wantVersion) {
84 util.okMessage();
85 }
86
87 // check to see if we have it already installed and if we do, just continue
88 if (!force && version) {
89 var bin = util.getInstallBinary(null, version);
90 if (bin) {
91 return callback(null, null, version, bin);
92 }
93 }
94
95 var total = parseInt(res.headers['content-length'], 10);
96 debug('download content-length: %d', total);
97
98 if (!total) {
99 return callback(errorlib.createError('com.appcelerator.install.download.invalid.content.length'));
100 }
101
102 bar = (!nobanner && process.stdout.isTTY && !process.env.TRAVIS)
103 && new ProgressBar('Downloading [:bar] :percent :etas', {
104 complete: util.isWindows() ? '█' : chalk.green('▤'),
105 incomplete: ' ',
106 width: Math.max(40, Math.round(process.stdout.columns / 2)),
107 total: total,
108 clear: true,
109 stream: process.stdout
110 });
111 var count = 0,
112 tickCount = 0,
113 tickDiff = total * 0.01;
114
115 util.stopSpinner();
116
117 if (!bar) {
118 util.waitMessage('Downloading ');
119 }
120
121 res.on('data', function (chunk) {
122 if (chunk.length) {
123 if (bar) {
124 tickCount += chunk.length;
125 if (tickCount > tickDiff) {
126 bar.tick(tickCount);
127 tickCount = 0;
128 }
129 }
130 stream.write(chunk);
131 hash.update(chunk);
132 count += chunk.length;
133 }
134 });
135
136 res.on('error', function (err) {
137 debug('download error %o', err);
138 try {
139 stream.end();
140 } catch (E) {
141 // ignore
142 }
143 pendingRequest = null;
144 callback(errorlib.createError('com.appcelerator.install.download.server.stream.error', err.message));
145 });
146
147 res.on('end', function () {
148 debug('download end');
149 if (bar) {
150 bar.tick(tickCount);
151 }
152 stream.end();
153 pendingRequest = null;
154 // check to make sure we downloaded all the bytes we needed too
155 // if not, this means the download failed and we should attempt to re-start it
156 if (count !== total) {
157 debug('download max retry');
158 if (bar) {
159 bar.terminate();
160 util.resetLine();
161 }
162 stream.end();
163 if (retryAttempts >= MAX_RETRIES) {
164 return callback(errorlib.createError('com.appcelerator.install.download.failed.retries.max', retryAttempts));
165 }
166 // re-open stream
167 stream = fs.createWriteStream(tmpfile);
168 var delay = retryAttempts * 2000;
169 // download failed, we should re-start
170 return setTimeout(function () {
171 download(force, wantVersion, tmpfile, stream, location, callback, true, retryAttempts + 1);
172 }, delay);
173 }
174 hash.end();
175 var checkshasum = hash.read();
176 debug('download checkshasum: %s', checkshasum);
177 // our downloaded file checksum should match what we uploaded, if not, this is a security violation
178 if (checkshasum !== shasum) {
179 return callback(errorlib.createError('com.appcelerator.install.download.failed.checksum', shasum, checkshasum));
180 } else if (!quiet) {
181 util.infoMessage('Validating security checksum ' + chalk.green(util.isWindows() ? 'OK' : '✓'));
182 }
183 process.nextTick(function () {
184 callback(null, tmpfile, version);
185 });
186 });
187 } else if (/^(408|500|503)$/.test(String(res.statusCode))) {
188 // some sort of error on the server, let's re-try again ...
189 // 408 is a server timeout
190 // 500 is a server error
191 // 503 is a server unavailable. this could be a deployment in progress
192 debug('download server error ... will retry');
193 stream.end();
194 if (bar) {
195 util.resetLine();
196 }
197 pendingRequest = null;
198 if (retryAttempts >= MAX_RETRIES) {
199 debug('download server error ... maxed out after %d attempts', retryAttempts);
200 return callback(errorlib.createError('com.appcelerator.install.download.server.unavailable'));
201 }
202 var delay = retryAttempts * 500;
203 debug('download server error ... retry delay %d ms', delay);
204 stream = fs.createWriteStream(tmpfile);
205 return setTimeout(function () {
206 download(quiet, force, wantVersion, tmpfile, stream, location, callback, true, retryAttempts + 1);
207 }, delay);
208 } else {
209 debug('download server unexpected error %d', res.statusCode);
210 stream.end();
211 if (bar) {
212 util.resetLine();
213 }
214 pendingRequest = null;
215 return callback(errorlib.createError('com.appcelerator.install.download.server.response.unexpected', res.statusCode));
216 }
217 });
218}
219
220exports.start = function (quiet, banner, force, location, wantVersion, callback) {
221 var tmpfile = path.join(tmpdir, 'appc-' + (+new Date()) + '.tar.gz'),
222 stream = fs.createWriteStream(tmpfile),
223 exitFn,
224 sigintFn,
225 pendingAbort;
226 function createCleanup(name) {
227 return function (exit) {
228 if (pendingRequest) {
229 try {
230 // abort the pending HTTP request so it will
231 // close the server socket
232 pendingRequest.abort();
233 } catch (E) {
234 // ignore
235 }
236 pendingRequest = null;
237 }
238 try {
239 if (fs.existSync(tmpfile)) {
240 fs.unlinkSync(tmpfile);
241 }
242 } catch (E) {
243 // ignore
244 }
245 if (name === 'SIGINT') {
246 pendingAbort = true;
247 process.removeListener('SIGINT', sigintFn);
248 util.abortMessage('Download');
249 } else if (name === 'exit') {
250 process.removeListener('exit', exitFn);
251 if (!pendingAbort) {
252 process.exit(exit);
253 }
254 } else {
255 process.removeListener('exit', exitFn);
256 process.removeListener('SIGINT', sigintFn);
257 }
258 };
259 }
260
261 // make sure we remove the file on shutdown
262 process.on('exit', (exitFn = createCleanup('exit')));
263 process.on('SIGINT', (sigintFn = createCleanup('SIGINT')));
264
265 // default banner is on for process downloads unless quiet or no banner
266 quiet = quiet === undefined ? false : quiet;
267 banner = (banner === undefined ? true : banner) && !(quiet);
268
269 debug('download start, quiet %d, banner %d', quiet, banner);
270
271 // run the download
272 download(quiet, force, wantVersion, tmpfile, stream, location, function () {
273 // remove clean listeners
274 createCleanup('done')();
275 // carry on... (pray)
276 return callback.apply(null, arguments);
277 }, !banner);
278};