1+ /* global Atomics, SharedArrayBuffer -- ES2017, used for the blocking wait in
2+ waitWhileBinaryBusySync; declared here rather than widening the lint env. */
13var https = require ( 'https' ) ,
24 fs = require ( 'fs' ) ,
35 path = require ( 'path' ) ,
@@ -71,6 +73,10 @@ function LocalBinary(){
7173 env . BROWSERSTACK_LOCAL_AUTH_TOKEN = this . key ;
7274 }
7375 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+ }
7480 if ( obj . stdout . length > 0 ) {
7581 this . sourceURL = obj . stdout . toString ( ) . replace ( / \n + $ / , '' ) ;
7682 this . downloadState . sourceURL = this . sourceURL ;
@@ -148,23 +154,60 @@ function LocalBinary(){
148154 this . downloadErrorMessage = errorMessagePrefix + ' : ' + errorMessage ;
149155 } ;
150156
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+
151181 this . retryBinaryDownload = function ( conf , destParentDir , callback , retries , binaryPath ) {
152182 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+ /* The async contract has to be completed or Local.start() waits forever.
186+ An empty path is the signal; the caller reports it. */
187+ if ( callback ) callback ( ) ;
188+ return ;
189+ }
190+ console . log ( 'Retrying Download. Retries left' , retries ) ;
191+
192+ /* Must stay synchronous: this return value is what downloadSync ->
193+ binaryPath() -> Local.getBinaryPath hands back. Retrying inside a callback
194+ returned undefined before the retry had done anything. */
195+ if ( ! callback ) {
196+ that . waitWhileBinaryBusySync ( binaryPath ) ;
197+ try { fs . unlinkSync ( binaryPath ) ; } catch ( err ) { /* missing or locked */ }
198+ return that . downloadSync ( conf , destParentDir , retries - 1 ) ;
199+ }
200+
201+ var attemptAsync = function ( waitsLeft ) {
202+ if ( waitsLeft > 0 && fs . existsSync ( binaryPath ) && that . isBinaryBusy ( binaryPath ) ) {
203+ console . log ( 'Binary is in use, waiting before retrying.' ) ;
204+ return setTimeout ( function ( ) { attemptAsync ( waitsLeft - 1 ) ; } , that . BUSY_WAIT_MS ) ;
205+ }
159206 fs . unlink ( binaryPath , function ( ) {
160- if ( ! callback ) {
161- return that . downloadSync ( conf , destParentDir , retries - 1 ) ;
162- }
163207 that . download ( conf , destParentDir , callback , retries - 1 ) ;
164208 } ) ;
165- } else {
166- console . error ( 'Number of retries to download exceeded.' ) ;
167- }
209+ } ;
210+ attemptAsync ( that . BUSY_MAX_WAITS ) ;
168211 } ;
169212
170213 this . downloadSync = function ( conf , destParentDir , retries ) {
@@ -198,6 +241,14 @@ function LocalBinary(){
198241 const userAgent = [ packageName , version ] . join ( '/' ) ;
199242 const env = Object . assign ( { 'USER_AGENT' : userAgent } , process . env ) ;
200243 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+ }
248+ if ( obj . error ) {
249+ that . binaryDownloadError ( 'Download failed with error' , util . format ( obj . error ) ) ;
250+ return that . retryBinaryDownload ( conf , destParentDir , null , retries , binaryPath ) ;
251+ }
201252 let output ;
202253 if ( obj . stdout . length > 0 ) {
203254 if ( fs . existsSync ( binaryPath ) ) {
@@ -221,7 +272,8 @@ function LocalBinary(){
221272 this . download = function ( conf , destParentDir , callback , retries ) {
222273 this . getDownloadPath ( conf , retries , ( err , downloadUrl ) => {
223274 if ( err ) {
224- 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 ( ) ;
225277 }
226278
227279 this . httpPath = downloadUrl ;
@@ -234,6 +286,21 @@ function LocalBinary(){
234286 var binaryPath = path . join ( destParentDir , destBinaryName ) ;
235287 var fileStream = fs . createWriteStream ( binaryPath ) ;
236288
289+ /* A failed open and the in-flight request can both report on the same
290+ attempt; one attempt must trigger at most one retry. */
291+ var retried = false ;
292+ var retryOnce = function ( prefix , err ) {
293+ that . binaryDownloadError ( prefix , util . format ( err ) ) ;
294+ if ( retried ) return ;
295+ retried = true ;
296+ that . retryBinaryDownload ( conf , destParentDir , callback , retries , binaryPath ) ;
297+ } ;
298+
299+ /* Same as lib/download.js: the open() failure lands first. */
300+ fileStream . on ( 'error' , function ( err ) {
301+ retryOnce ( 'Got Error while downloading binary file' , err ) ;
302+ } ) ;
303+
237304 var options = url . parse ( this . httpPath ) ;
238305 if ( conf . proxyHost && conf . proxyPort ) {
239306 options . agent = new HttpsProxyAgent ( {
@@ -267,21 +334,18 @@ function LocalBinary(){
267334 }
268335
269336 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 ) ;
337+ retryOnce ( 'Got Error in binary download response' , err ) ;
276338 } ) ;
277339 fileStream . on ( 'close' , function ( ) {
340+ /* node emits 'close' after 'error' too, so without this a failed
341+ attempt reports success alongside the retry it just started. */
342+ if ( retried ) return ;
278343 fs . chmod ( binaryPath , '0755' , function ( ) {
279344 callback ( binaryPath ) ;
280345 } ) ;
281346 } ) ;
282347 } ) . on ( 'error' , function ( err ) {
283- that . binaryDownloadError ( 'Got Error in binary downloading request' , util . format ( err ) ) ;
284- that . retryBinaryDownload ( conf , destParentDir , callback , retries , binaryPath ) ;
348+ retryOnce ( 'Got Error in binary downloading request' , err ) ;
285349 } ) ;
286350 } ) ;
287351 } ;
0 commit comments