@@ -394,7 +394,33 @@ export async function parseOptionsAndRunCLI() {
394394 ] ,
395395 } as RunCLIArgs ;
396396
397- await runCLI ( cliArgs ) ;
397+ const cliServer = await runCLI ( cliArgs ) ;
398+ if ( cliServer === undefined ) {
399+ // No server was started, so we are done with our work.
400+ process . exit ( 0 ) ;
401+ }
402+
403+ const cleanUpCliAndExit = ( ( ) => {
404+ // Remember we are already cleaning up to preclude the possibility
405+ // of multiple, conflicting cleanup attempts.
406+ let promiseToCleanup : Promise < void > ;
407+
408+ return async ( ) => {
409+ if ( promiseToCleanup !== undefined ) {
410+ promiseToCleanup = cliServer [ Symbol . asyncDispose ] ( ) ;
411+ }
412+ await promiseToCleanup ;
413+ process . exit ( 0 ) ;
414+ } ;
415+ } ) ( ) ;
416+
417+ // Playground CLI server must be killed to exit. From the terminal,
418+ // this may occur via Ctrl+C which sends SIGINT. Let's handle both
419+ // SIGINT and SIGTERM (the default kill signal) to make sure we
420+ // clean up after ourselves even if this process is being killed.
421+ // NOTE: Windows does not support SIGTERM, but Node.js provides some emulation.
422+ process . on ( 'SIGINT' , cleanUpCliAndExit ) ;
423+ process . on ( 'SIGTERM' , cleanUpCliAndExit ) ;
398424 } catch ( e ) {
399425 if ( ! ( e instanceof Error ) ) {
400426 throw e ;
@@ -437,7 +463,6 @@ export interface RunCLIArgs {
437463 autoMount ?: string ;
438464 experimentalMultiWorker ?: number ;
439465 experimentalTrace ?: boolean ;
440- exitOnPrimaryWorkerCrash ?: boolean ;
441466 internalCookieStore ?: boolean ;
442467 'additional-blueprint-steps' ?: any [ ] ;
443468 xdebug ?: boolean | { ideKey ?: string } ;
@@ -492,7 +517,17 @@ const italic = (text: string) =>
492517const highlight = ( text : string ) =>
493518 process . stdout . isTTY ? `\x1b[33m${ text } \x1b[0m` : text ;
494519
495- export async function runCLI ( args : RunCLIArgs ) : Promise < RunCLIServer > {
520+ // These overloads are declared for convenience so runCLI() can return
521+ // different things depending on the CLI command without forcing the
522+ // callers (mostly automated tests) to check return values.
523+ export async function runCLI (
524+ args : RunCLIArgs & { command : 'build-snapshot' | 'run-blueprint' }
525+ ) : Promise < void > ;
526+ export async function runCLI (
527+ args : RunCLIArgs & { command : 'server' }
528+ ) : Promise < RunCLIServer > ;
529+ export async function runCLI ( args : RunCLIArgs ) : Promise < RunCLIServer | void > ;
530+ export async function runCLI ( args : RunCLIArgs ) : Promise < RunCLIServer | void > {
496531 let loadBalancer : LoadBalancer ;
497532 let playground : RemoteAPI < PlaygroundCliWorker > ;
498533
@@ -562,7 +597,7 @@ export async function runCLI(args: RunCLIArgs): Promise<RunCLIServer> {
562597
563598 return startServer ( {
564599 port : args [ 'port' ] as number ,
565- onBind : async ( server : Server , port : number ) : Promise < RunCLIServer > => {
600+ onBind : async ( server : Server , port : number ) => {
566601 const host = '127.0.0.1' ;
567602 const serverUrl = `http://${ host } :${ port } ` ;
568603 const siteUrl = args [ 'site-url' ] || serverUrl ;
@@ -584,10 +619,10 @@ export async function runCLI(args: RunCLIArgs): Promise<RunCLIServer> {
584619 * because we don't have to create or maintain multiple copies of the same files.
585620 */
586621 const tempDirNameDelimiter = '-playground-cli-site-' ;
587- const nativeDirPath = await createPlaygroundCliTempDir (
622+ const nativeDir = await createPlaygroundCliTempDir (
588623 tempDirNameDelimiter
589624 ) ;
590- logger . debug ( `Native temp dir for VFS root: ${ nativeDirPath } ` ) ;
625+ logger . debug ( `Native temp dir for VFS root: ${ nativeDir . path } ` ) ;
591626
592627 const IDEConfigName = 'WP Playground CLI - Listen for Xdebug' ;
593628
@@ -602,7 +637,7 @@ export async function runCLI(args: RunCLIArgs): Promise<RunCLIServer> {
602637 // directory and add the new IDE config.
603638 if ( args . xdebug && args . experimentalUnsafeIdeIntegration ) {
604639 await createPlaygroundCliTempDirSymlink (
605- nativeDirPath ,
640+ nativeDir . path ,
606641 symlinkPath ,
607642 process . platform
608643 ) ;
@@ -696,17 +731,15 @@ export async function runCLI(args: RunCLIArgs): Promise<RunCLIServer> {
696731
697732 console . log ( '' ) ;
698733 } catch ( error ) {
699- logger . error (
700- 'Could not configure Xdebug:' ,
701- ( error as Error ) ?. message
702- ) ;
703- process . exit ( 1 ) ;
734+ throw new Error ( 'Could not configure Xdebug' , {
735+ cause : error ,
736+ } ) ;
704737 }
705738 }
706739
707740 // We do not know the system temp dir,
708741 // but we can try to infer from the location of the current temp dir.
709- const tempDirRoot = path . dirname ( nativeDirPath ) ;
742+ const tempDirRoot = path . dirname ( nativeDir . path ) ;
710743
711744 const twoDaysInMillis = 2 * 24 * 60 * 60 * 1000 ;
712745 const tempDirStaleAgeInMillis = twoDaysInMillis ;
@@ -721,7 +754,7 @@ export async function runCLI(args: RunCLIArgs): Promise<RunCLIServer> {
721754
722755 // NOTE: We do not add mount declarations for /internal here
723756 // because it will be mounted as part of php-wasm init.
724- const nativeInternalDirPath = path . join ( nativeDirPath , 'internal' ) ;
757+ const nativeInternalDirPath = path . join ( nativeDir . path , 'internal' ) ;
725758 mkdirSync ( nativeInternalDirPath ) ;
726759
727760 const userProvidableNativeSubdirs = [
@@ -746,7 +779,7 @@ export async function runCLI(args: RunCLIArgs): Promise<RunCLIServer> {
746779 // The user hasn't requested mounting a different native dir for this path,
747780 // so let's create a mount from within our native temp dir.
748781 const nativeSubdirPath = path . join (
749- nativeDirPath ,
782+ nativeDir . path ,
750783 subdirName
751784 ) ;
752785 mkdirSync ( nativeSubdirPath ) ;
@@ -799,26 +832,48 @@ export async function runCLI(args: RunCLIArgs): Promise<RunCLIServer> {
799832 }
800833 }
801834
835+ // Remember whether we are already disposing so we can avoid:
836+ // - we can avoid multiple, conflicting dispose attempts
837+ // - logging that a worker exited while the CLI itself is exiting
838+ let disposing = false ;
839+ const disposeCLI = async function disposeCLI ( ) {
840+ if ( disposing ) {
841+ return ;
842+ }
843+
844+ disposing = true ;
845+ await Promise . all (
846+ playgroundsToCleanUp . map ( async ( { playground, worker } ) => {
847+ await playground . dispose ( ) ;
848+ await worker . terminate ( ) ;
849+ } )
850+ ) ;
851+ if ( server ) {
852+ await new Promise ( ( resolve ) => server . close ( resolve ) ) ;
853+ }
854+ await nativeDir . cleanup ( ) ;
855+ } ;
856+
802857 // Kick off worker threads now to save time later.
803858 // There is no need to wait for other async processes to complete.
804859 const promisedWorkers = spawnWorkerThreads (
805860 totalWorkerCount ,
806861 handler . getWorkerType ( ) ,
807- ( { exitCode, isMain, workerIndex } ) => {
808- if ( exitCode === 0 ) {
862+ ( { exitCode, workerIndex } ) => {
863+ // We are already disposing, so worker exit is expected
864+ // and does not need to be logged.
865+ if ( disposing ) {
866+ return ;
867+ }
868+
869+ if ( exitCode !== 0 ) {
809870 return ;
810871 }
872+
811873 logger . error (
812874 `Worker ${ workerIndex } exited with code ${ exitCode } \n`
813875 ) ;
814- // If the primary worker crashes, exit the entire process.
815- if ( ! isMain ) {
816- return ;
817- }
818- if ( ! args . exitOnPrimaryWorkerCrash ) {
819- return ;
820- }
821- process . exit ( 1 ) ;
876+ // @TODO : Should we respawn the worker if it exited with an error and the CLI is not shutting down?
822877 }
823878 ) ;
824879
@@ -869,10 +924,12 @@ export async function runCLI(args: RunCLIArgs): Promise<RunCLIServer> {
869924 if ( args . command === 'build-snapshot' ) {
870925 await zipSite ( playground , args . outfile as string ) ;
871926 logger . log ( `WordPress exported to ${ args . outfile } ` ) ;
872- process . exit ( 0 ) ;
927+ await disposeCLI ( ) ;
928+ return ;
873929 } else if ( args . command === 'run-blueprint' ) {
874930 logger . log ( `Blueprint executed` ) ;
875- process . exit ( 0 ) ;
931+ await disposeCLI ( ) ;
932+ return ;
876933 }
877934
878935 if (
@@ -927,17 +984,7 @@ export async function runCLI(args: RunCLIArgs): Promise<RunCLIServer> {
927984 playground,
928985 server,
929986 serverUrl,
930- [ Symbol . asyncDispose ] : async function disposeCLI ( ) {
931- await Promise . all (
932- playgroundsToCleanUp . map (
933- async ( { playground, worker } ) => {
934- await playground . dispose ( ) ;
935- await worker . terminate ( ) ;
936- }
937- )
938- ) ;
939- await new Promise ( ( resolve ) => server . close ( resolve ) ) ;
940- } ,
987+ [ Symbol . asyncDispose ] : disposeCLI ,
941988 workerThreadCount : totalWorkerCount ,
942989 } ;
943990 } catch ( error ) {
@@ -993,19 +1040,14 @@ export type SpawnedWorker = {
9931040async function spawnWorkerThreads (
9941041 count : number ,
9951042 workerType : WorkerType ,
996- onWorkerExit : ( options : {
997- exitCode : number ;
998- isMain : boolean ;
999- workerIndex : number ;
1000- } ) => void
1043+ onWorkerExit : ( options : { exitCode : number ; workerIndex : number } ) => void
10011044) : Promise < SpawnedWorker [ ] > {
10021045 const promises = [ ] ;
10031046 for ( let i = 0 ; i < count ; i ++ ) {
10041047 const worker = await spawnWorkerThread ( workerType ) ;
10051048 const onExit : ( code : number ) => void = ( code : number ) => {
10061049 onWorkerExit ( {
10071050 exitCode : code ,
1008- isMain : i === 0 ,
10091051 workerIndex : i ,
10101052 } ) ;
10111053 } ;
0 commit comments