Skip to content

Commit 2ee720a

Browse files
pranay-v29claude
andcommitted
Replace an unusable binary rather than re-spawning it
Addresses the three findings from review. The recovery added for a corrupt binary only worked when the unlink succeeded. When it failed -- the locked-file case this branch is about -- binaryPath() handed the same file straight back, because checkPath uses X_OK and Windows treats that as F_OK, so the binary was re-spawned for every remaining retry with no wait. Wait for the lock before replacing it, and stop rather than retry when the file survives. download() returned on a source-url error without calling back, so an invalid key or network failure left Local.start() waiting. Pre-existing, but the same contract the previous commit closed. download.js printed Done from the close handler, which node emits after error too, so downloadSync accepted a partially written binary as a completed download. Guard the log, and treat a non-zero exit status as a failed attempt before inspecting stdout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 0a426a3 commit 2ee720a

4 files changed

Lines changed: 66 additions & 8 deletions

File tree

‎lib/Local.js‎

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,12 @@ function Local(){
8484
if(that.retriesLeft > 0) {
8585
console.log('Retrying Binary Download. Retries Left', that.retriesLeft);
8686
that.retriesLeft -= 1;
87-
/* EPERM on a locked file threw straight out of startSync. */
87+
if(that.binary) that.binary.waitWhileBinaryBusySync(that.binaryPath);
8888
try { fs.unlinkSync(that.binaryPath); } catch(err) { /* ignored */ }
89+
/* Still there: binaryPath() would hand back the same unusable file. */
90+
if(fs.existsSync(that.binaryPath)) {
91+
return new LocalError(binaryDownloadErrorMessage);
92+
}
8993
delete(that.binaryPath);
9094
that.binaryDownloadState.errorMessage = binaryDownloadErrorMessage;
9195
that.binaryDownloadState.fallbackEnabled = true;
@@ -125,11 +129,21 @@ function Local(){
125129
if(that.retriesLeft > 0) {
126130
console.log('Retrying Binary Download. Retries Left', that.retriesLeft);
127131
that.retriesLeft -= 1;
128-
try { fs.unlinkSync(that.binaryPath); } catch(err) { /* ignored */ }
129-
delete(that.binaryPath);
130-
that.binaryDownloadState.errorMessage = binaryDownloadErrorMessage;
131-
that.binaryDownloadState.fallbackEnabled = true;
132-
that.start(options, callback);
132+
var replace = function(waitsLeft) {
133+
if(waitsLeft > 0 && fs.existsSync(that.binaryPath) &&
134+
that.binary && that.binary.isBinaryBusy(that.binaryPath)) {
135+
return setTimeout(function() { replace(waitsLeft - 1); }, 1000);
136+
}
137+
try { fs.unlinkSync(that.binaryPath); } catch(err) { /* ignored */ }
138+
if(fs.existsSync(that.binaryPath)) {
139+
return callback(new LocalError(binaryDownloadErrorMessage));
140+
}
141+
delete(that.binaryPath);
142+
that.binaryDownloadState.errorMessage = binaryDownloadErrorMessage;
143+
that.binaryDownloadState.fallbackEnabled = true;
144+
that.start(options, callback);
145+
};
146+
replace(3);
133147
return;
134148
} else {
135149
callback(new LocalError(error.toString()));

‎lib/LocalBinary.js‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,10 @@ function LocalBinary(){
241241
const userAgent = [packageName, version].join('/');
242242
const env = Object.assign({ 'USER_AGENT': userAgent }, process.env);
243243
const obj = childProcess.spawnSync(cmd, opts, { env: env });
244+
if(obj.status !== 0) {
245+
that.binaryDownloadError('Download failed with status', String(obj.status));
246+
return that.retryBinaryDownload(conf, destParentDir, null, retries, binaryPath);
247+
}
244248
if(obj.error) {
245249
that.binaryDownloadError('Download failed with error', util.format(obj.error));
246250
return that.retryBinaryDownload(conf, destParentDir, null, retries, binaryPath);
@@ -268,7 +272,8 @@ function LocalBinary(){
268272
this.download = function(conf, destParentDir, callback, retries){
269273
this.getDownloadPath(conf, retries, (err, downloadUrl) => {
270274
if(err) {
271-
return console.error('Unable to fetch the source url to download the binary with error: ', err);
275+
console.error('Unable to fetch the source url to download the binary with error: ', err);
276+
return callback();
272277
}
273278

274279
this.httpPath = downloadUrl;

‎lib/download.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ request = https.get(options, function (response) {
6565
console.error('Got Error in binary download response', err);
6666
});
6767
fileStream.on('close', function () {
68+
if(process.exitCode === 1) return; // errored; not a completed download
6869
console.log('Done');
6970
});
7071
}).on('error', function(err) {

‎test/local_binary_busy_download.js‎

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ var expect = require('expect.js'),
33
fs = require('fs'),
44
os = require('os'),
55
path = require('path'),
6-
LocalBinary = require('../lib/LocalBinary');
6+
LocalBinary = require('../lib/LocalBinary'),
7+
browserstack = require('../index');
78

89
// Regression tests for LOC-7420.
910
//
@@ -93,6 +94,42 @@ describe('LocalBinary busy-binary download handling', function () {
9394
});
9495
});
9596

97+
describe('source url failure', function () {
98+
it('completes the callback when the download url cannot be fetched', function (done) {
99+
var binary = new LocalBinary();
100+
binary.getDownloadPath = function (conf, retries, cb) { cb(new Error('invalid key')); };
101+
binary.download({}, os.tmpdir(), function (binaryPath) {
102+
expect(binaryPath).to.be(undefined);
103+
done();
104+
}, 9);
105+
});
106+
});
107+
108+
describe('unremovable binary', function () {
109+
// An unusable binary whose unlink fails used to be handed straight back by
110+
// binaryPath() and re-spawned for every remaining retry.
111+
it('does not retry when the binary cannot be replaced', function () {
112+
var dir = fs.mkdtempSync(path.join(os.tmpdir(), 'bs-local-')),
113+
binaryPath = path.join(dir, 'BrowserStackLocal');
114+
fs.writeFileSync(binaryPath, 'not executable', { mode: 0o644 });
115+
fs.chmodSync(dir, 0o555); // so the unlink fails
116+
117+
try {
118+
var bsLocal = new browserstack.Local();
119+
bsLocal.binaryPath = binaryPath;
120+
var result = bsLocal.startSync({ key: 'dummy-key' });
121+
122+
expect(result).to.be.a(Object);
123+
expect(result.message).to.contain('Error while trying to execute binary');
124+
expect(bsLocal.retriesLeft).to.equal(8); // one attempt, not nine
125+
} finally {
126+
fs.chmodSync(dir, 0o755);
127+
fs.unlinkSync(binaryPath);
128+
fs.rmdirSync(dir);
129+
}
130+
});
131+
});
132+
96133
describe('isBinaryBusy', function () {
97134
it('reports a readable file as free', function () {
98135
var binary = new LocalBinary(),
@@ -129,6 +166,7 @@ describe('LocalBinary busy-binary download handling', function () {
129166
expect(stderr).to.contain('Got Error while downloading binary file');
130167
// The signature of the old defect: node's unhandled-'error' bail-out.
131168
expect(stderr).to.not.contain('Unhandled \'error\' event');
169+
expect(obj.stdout.toString()).to.not.contain('Done');
132170
expect(obj.status).to.equal(1);
133171

134172
fs.rmdirSync(target);

0 commit comments

Comments
 (0)