@@ -5,17 +5,20 @@ import { BrowserStackError } from "@/error";
5
5
import { ensureDirExists } from "@/fs-utils" ;
6
6
import { BrowserStack , LocalTestingBinaryOptions } from "@/index.node" ;
7
7
import { writeFileAtomic } from "@/write-file-atomic" ;
8
+ import cp from "node:child_process" ;
8
9
import { readFile } from "node:fs/promises" ;
9
10
import { createRequire } from "node:module" ;
10
11
import { homedir , tmpdir } from "node:os" ;
11
12
import { join } from "node:path" ;
13
+ import { onExit } from "signal-exit" ;
12
14
13
15
const require = createRequire ( import . meta. url ) ;
14
16
15
17
enum BrowserStackLocalAction {
16
18
start = "start" ,
17
19
stop = "stop" ,
18
20
list = "list" ,
21
+ runWith = "run-with" ,
19
22
}
20
23
21
24
interface Logger {
@@ -67,6 +70,8 @@ async function start(
67
70
await writeStatusFile ( statusPath , localIdentifiers , entries ) ;
68
71
logger . info ( `${ localIdentifier } : ${ status } ` ) ;
69
72
}
73
+
74
+ return localIdentifier ;
70
75
}
71
76
72
77
async function stopInstance (
@@ -156,9 +161,61 @@ async function stop(
156
161
*/
157
162
async function list ( statusPath : string , logger : Logger = globalThis . console ) {
158
163
const { localIdentifiers = [ ] } = await readOrCreateStatusFile ( statusPath ) ;
164
+ localIdentifiers . forEach ( ( localIdentifier ) => logger . info ( localIdentifier ) ) ;
165
+ }
166
+
167
+ /**
168
+ * Runs the local testing binary before a user-provided command and stops it after the command has finished.
169
+ * Also sets the BROWSERSTACK_LOCAL_ID and BROWSERSTACK_LOCAL_IDENTIFIER environment variables.
170
+ *
171
+ * @param options - The options for running the local testing binary.
172
+ * @param statusPath - The path to the status file.
173
+ * @param runWithArgs - The arguments to run the local testing binary with.
174
+ * @param logger - The logger instance.
175
+ */
176
+ async function runWith (
177
+ options : LocalTestingBinaryOptions ,
178
+ statusPath : string ,
179
+ runWithArgs : string [ ] ,
180
+ logger : Logger
181
+ ) {
182
+ const localIdentifier = await start ( options , statusPath , logger ) ;
183
+ let childExitCode : number ;
184
+
185
+ const exitHandler = async ( code ?: number | null ) => {
186
+ await stop ( { ...options , localIdentifier } , statusPath , logger ) ;
187
+ process . exit ( typeof code === "number" ? code : childExitCode ) ;
188
+ } ;
189
+
190
+ const removeOnExitHandler = onExit ( ( ) => {
191
+ ( async ( ) => await exitHandler ( ) ) ( ) ;
192
+ } ) ;
193
+
194
+ try {
195
+ const [ cmd , ...args ] = runWithArgs ;
196
+ const childProcess = cp . spawnSync ( cmd , args , {
197
+ cwd : process . cwd ( ) ,
198
+ stdio : "inherit" ,
199
+ windowsHide : true ,
200
+ env : {
201
+ ...process . env ,
202
+ BROWSERSTACK_LOCAL_ID : localIdentifier ,
203
+ BROWSERSTACK_LOCAL_IDENTIFIER : localIdentifier ,
204
+ } ,
205
+ } ) ;
159
206
160
- for ( const localIdentifier of localIdentifiers ) {
161
- logger . info ( localIdentifier ) ;
207
+ childExitCode = childProcess . status ?? ( childProcess . error ? 1 : 0 ) ;
208
+ } catch ( err ) {
209
+ childExitCode = 1 ;
210
+
211
+ if ( err instanceof Error ) {
212
+ logger . error ( err ?. message ) ;
213
+ } else {
214
+ logger . error ( `An unexpected error occurred: ${ err } ` ) ;
215
+ }
216
+ } finally {
217
+ removeOnExitHandler ( ) ;
218
+ await exitHandler ( ) ;
162
219
}
163
220
}
164
221
@@ -334,19 +391,23 @@ async function writeStatusFile(
334
391
null ,
335
392
2
336
393
) ,
337
- { encoding : fileEncoding } ,
394
+ { encoding : fileEncoding }
338
395
) ;
339
396
}
340
397
341
398
export async function main (
342
399
inputArgs : string [ ] = process . argv . slice ( 2 ) ,
343
- logger : Logger = globalThis . console
400
+ logger : Logger = globalThis . console ,
401
+ cmdSeparator : string = "--"
344
402
) {
345
403
try {
346
404
const args = inputArgs . map ( ( arg ) => arg . trim ( ) ) ;
347
405
const action = ensureValidAction ( args [ 0 ] ) ;
348
- const localIdentifier = resolveEnvLocalIdentifier ( ) ?? args [ 1 ] ;
349
- const key = ensureKeyExists ( args [ 2 ] ) ;
406
+ const localIdentifier =
407
+ resolveEnvLocalIdentifier ( ) ??
408
+ ( args [ 1 ] === cmdSeparator ? undefined : args [ 1 ] ) ;
409
+
410
+ const key = ensureKeyExists ( args [ 2 ] === cmdSeparator ? undefined : args [ 2 ] ) ;
350
411
const binHome = await ensureBinHomeExists ( ) ;
351
412
const statusPath = join ( binHome , "status.json" ) ;
352
413
@@ -363,6 +424,29 @@ export async function main(
363
424
await list ( statusPath , logger ) ;
364
425
break ;
365
426
}
427
+ case BrowserStackLocalAction . runWith : {
428
+ const cmdStartIndex = args . findIndex ( ( arg ) => arg === cmdSeparator ) ;
429
+ if ( cmdStartIndex === - 1 ) {
430
+ throw new BrowserStackError (
431
+ `Invalid run-with command: no command separator ${ cmdSeparator } found`
432
+ ) ;
433
+ }
434
+
435
+ const runWithArgs = args . slice ( cmdStartIndex + 1 ) ;
436
+ if ( ! runWithArgs . length ) {
437
+ throw new BrowserStackError (
438
+ "Invalid run-with command: no command provided"
439
+ ) ;
440
+ }
441
+
442
+ // resolves and exits by itself
443
+ return await runWith (
444
+ { key, binHome, localIdentifier } ,
445
+ statusPath ,
446
+ runWithArgs ,
447
+ logger
448
+ ) ;
449
+ }
366
450
default :
367
451
throw new BrowserStackError (
368
452
`Invalid action: ${ action } (valid actions: start, stop, list)`
0 commit comments