|
| 1 | +/* global Atomics, SharedArrayBuffer -- ES2017, used for the blocking wait in |
| 2 | + waitWhileBinaryBusySync; declared here rather than widening the lint env. */ |
1 | 3 | var https = require('https'), |
2 | 4 | fs = require('fs'), |
3 | 5 | path = require('path'), |
@@ -71,6 +73,10 @@ function LocalBinary(){ |
71 | 73 | env.BROWSERSTACK_LOCAL_AUTH_TOKEN = this.key; |
72 | 74 | } |
73 | 75 | const obj = childProcess.spawnSync(cmd, opts, { env: env }); |
| 76 | + /* stdout is null on a spawn failure; reading .length masked the real cause. */ |
| 77 | + if(obj.error) { |
| 78 | + throw(util.format(obj.error)); |
| 79 | + } |
74 | 80 | if(obj.stdout.length > 0) { |
75 | 81 | this.sourceURL = obj.stdout.toString().replace(/\n+$/, ''); |
76 | 82 | this.downloadState.sourceURL = this.sourceURL; |
@@ -148,23 +154,57 @@ function LocalBinary(){ |
148 | 154 | this.downloadErrorMessage = errorMessagePrefix + ' : ' + errorMessage; |
149 | 155 | }; |
150 | 156 |
|
| 157 | + /* A locked binary is transient on Windows (AV scan, a tunnel still releasing |
| 158 | + its handle), not a corrupt one. Mirrors the CLI binary's existing probe. */ |
| 159 | + this.BUSY_ERROR_CODES = ['EBUSY', 'EPERM', 'ETXTBSY', 'EACCES']; |
| 160 | + this.BUSY_MAX_WAITS = 3; |
| 161 | + this.BUSY_WAIT_MS = 1000; |
| 162 | + |
| 163 | + this.isBinaryBusy = function(binaryPath) { |
| 164 | + try { |
| 165 | + fs.closeSync(fs.openSync(binaryPath, 'r+')); |
| 166 | + return false; |
| 167 | + } catch(err) { |
| 168 | + return this.BUSY_ERROR_CODES.indexOf(err.code) !== -1; |
| 169 | + } |
| 170 | + }; |
| 171 | + |
| 172 | + /* Blocking by design: the sync path has no event loop to come back to. */ |
| 173 | + this.waitWhileBinaryBusySync = function(binaryPath) { |
| 174 | + for(var i = 0; i < this.BUSY_MAX_WAITS; i++) { |
| 175 | + if(!fs.existsSync(binaryPath) || !this.isBinaryBusy(binaryPath)) return; |
| 176 | + console.log('Binary is in use, waiting before retrying.'); |
| 177 | + Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, this.BUSY_WAIT_MS); |
| 178 | + } |
| 179 | + }; |
| 180 | + |
151 | 181 | this.retryBinaryDownload = function(conf, destParentDir, callback, retries, binaryPath) { |
152 | 182 | var that = this; |
153 | | - if(retries > 0) { |
154 | | - console.log('Retrying Download. Retries left', retries); |
155 | | - /* Single unlink instead of stat-then-unlinkSync: the gap between the two |
156 | | - let a concurrent writer swap the file, and a failing unlinkSync threw |
157 | | - out of the stat callback where it could not be caught. A missing file |
158 | | - is the expected case here, so any error is ignored. */ |
| 183 | + if(retries <= 0) { |
| 184 | + console.error('Number of retries to download exceeded.'); |
| 185 | + return; |
| 186 | + } |
| 187 | + console.log('Retrying Download. Retries left', retries); |
| 188 | + |
| 189 | + /* Must stay synchronous: this return value is what downloadSync -> |
| 190 | + binaryPath() -> Local.getBinaryPath hands back. Retrying inside a callback |
| 191 | + returned undefined before the retry had done anything. */ |
| 192 | + if(!callback) { |
| 193 | + that.waitWhileBinaryBusySync(binaryPath); |
| 194 | + try { fs.unlinkSync(binaryPath); } catch(err) { /* missing or locked */ } |
| 195 | + return that.downloadSync(conf, destParentDir, retries - 1); |
| 196 | + } |
| 197 | + |
| 198 | + var attemptAsync = function(waitsLeft) { |
| 199 | + if(waitsLeft > 0 && fs.existsSync(binaryPath) && that.isBinaryBusy(binaryPath)) { |
| 200 | + console.log('Binary is in use, waiting before retrying.'); |
| 201 | + return setTimeout(function() { attemptAsync(waitsLeft - 1); }, that.BUSY_WAIT_MS); |
| 202 | + } |
159 | 203 | fs.unlink(binaryPath, function() { |
160 | | - if(!callback) { |
161 | | - return that.downloadSync(conf, destParentDir, retries - 1); |
162 | | - } |
163 | 204 | that.download(conf, destParentDir, callback, retries - 1); |
164 | 205 | }); |
165 | | - } else { |
166 | | - console.error('Number of retries to download exceeded.'); |
167 | | - } |
| 206 | + }; |
| 207 | + attemptAsync(that.BUSY_MAX_WAITS); |
168 | 208 | }; |
169 | 209 |
|
170 | 210 | this.downloadSync = function(conf, destParentDir, retries) { |
@@ -198,6 +238,10 @@ function LocalBinary(){ |
198 | 238 | const userAgent = [packageName, version].join('/'); |
199 | 239 | const env = Object.assign({ 'USER_AGENT': userAgent }, process.env); |
200 | 240 | const obj = childProcess.spawnSync(cmd, opts, { env: env }); |
| 241 | + if(obj.error) { |
| 242 | + that.binaryDownloadError('Download failed with error', util.format(obj.error)); |
| 243 | + return that.retryBinaryDownload(conf, destParentDir, null, retries, binaryPath); |
| 244 | + } |
201 | 245 | let output; |
202 | 246 | if(obj.stdout.length > 0) { |
203 | 247 | if(fs.existsSync(binaryPath)){ |
@@ -234,6 +278,21 @@ function LocalBinary(){ |
234 | 278 | var binaryPath = path.join(destParentDir, destBinaryName); |
235 | 279 | var fileStream = fs.createWriteStream(binaryPath); |
236 | 280 |
|
| 281 | + /* A failed open and the in-flight request can both report on the same |
| 282 | + attempt; one attempt must trigger at most one retry. */ |
| 283 | + var retried = false; |
| 284 | + var retryOnce = function(prefix, err) { |
| 285 | + that.binaryDownloadError(prefix, util.format(err)); |
| 286 | + if(retried) return; |
| 287 | + retried = true; |
| 288 | + that.retryBinaryDownload(conf, destParentDir, callback, retries, binaryPath); |
| 289 | + }; |
| 290 | + |
| 291 | + /* Same as lib/download.js: the open() failure lands first. */ |
| 292 | + fileStream.on('error', function (err) { |
| 293 | + retryOnce('Got Error while downloading binary file', err); |
| 294 | + }); |
| 295 | + |
237 | 296 | var options = url.parse(this.httpPath); |
238 | 297 | if(conf.proxyHost && conf.proxyPort) { |
239 | 298 | options.agent = new HttpsProxyAgent({ |
@@ -267,21 +326,15 @@ function LocalBinary(){ |
267 | 326 | } |
268 | 327 |
|
269 | 328 | response.on('error', function(err) { |
270 | | - that.binaryDownloadError('Got Error in binary download response', util.format(err)); |
271 | | - that.retryBinaryDownload(conf, destParentDir, callback, retries, binaryPath); |
272 | | - }); |
273 | | - fileStream.on('error', function (err) { |
274 | | - that.binaryDownloadError('Got Error while downloading binary file', util.format(err)); |
275 | | - that.retryBinaryDownload(conf, destParentDir, callback, retries, binaryPath); |
| 329 | + retryOnce('Got Error in binary download response', err); |
276 | 330 | }); |
277 | 331 | fileStream.on('close', function () { |
278 | 332 | fs.chmod(binaryPath, '0755', function() { |
279 | 333 | callback(binaryPath); |
280 | 334 | }); |
281 | 335 | }); |
282 | 336 | }).on('error', function(err) { |
283 | | - that.binaryDownloadError('Got Error in binary downloading request', util.format(err)); |
284 | | - that.retryBinaryDownload(conf, destParentDir, callback, retries, binaryPath); |
| 337 | + retryOnce('Got Error in binary downloading request', err); |
285 | 338 | }); |
286 | 339 | }); |
287 | 340 | }; |
|
0 commit comments