@@ -33,7 +33,7 @@ import path from 'path';
3333import { rootCertificates } from 'tls' ;
3434import { MessageChannel , type MessagePort , parentPort } from 'worker_threads' ;
3535import { jspi } from 'wasm-feature-detect' ;
36- import { type RunCLIArgs } from '../run-cli' ;
36+ import { type RunCLIArgs , spawnWorkerThread } from '../run-cli' ;
3737import type {
3838 PhpIniOptions ,
3939 PHPInstanceCreatedHook ,
@@ -469,8 +469,26 @@ export class PlaygroundCliBlueprintV2Worker extends PHPWorker {
469469 constants,
470470 phpIniEntries,
471471 cookieStore : false ,
472- spawnHandler : ( getPHPInstance ) =>
473- sandboxedSpawnHandlerFactory ( getPHPInstance ) ,
472+ spawnHandler : ( ) =>
473+ sandboxedSpawnHandlerFactory ( ( ) =>
474+ createPHPWorker (
475+ {
476+ siteUrl,
477+ allow,
478+ phpVersion,
479+ phpIniEntries,
480+ constants,
481+ createFiles,
482+ firstProcessId,
483+ processIdSpaceLength,
484+ trace,
485+ nativeInternalDirPath,
486+ withXdebug,
487+ onPHPInstanceCreated,
488+ } ,
489+ this . fileLockManager !
490+ )
491+ ) ,
474492 } ) ;
475493 this . __internal_setRequestHandler ( requestHandler ) ;
476494
@@ -490,6 +508,66 @@ export class PlaygroundCliBlueprintV2Worker extends PHPWorker {
490508 }
491509}
492510
511+ /**
512+ * Spawns a new PHP process to be used in the PHP spawn handler (in proc_open() etc. calls).
513+ * It boots from this worker-thread-v2.ts file, but is a separate process.
514+ *
515+ * We explicitly avoid using PHPProcessManager.acquirePHPInstance() here.
516+ *
517+ * Why?
518+ *
519+ * Because each PHP instance acquires actual OS-level file locks via fcntl() and LockFileEx()
520+ * syscalls. Running multiple PHP instances from the same OS process would allow them to
521+ * acquire overlapping locks. Running every PHP instance in a separate OS process ensures
522+ * any locks that overlap between PHP instances conflict with each other as expected.
523+ *
524+ * @param options - The options for the worker.
525+ * @param fileLockManager - The file lock manager to use.
526+ * @returns A promise that resolves to the PHP worker.
527+ */
528+ async function createPHPWorker (
529+ options : WorkerBootRequestHandlerOptions ,
530+ fileLockManager : FileLockManager | RemoteAPI < FileLockManager >
531+ ) {
532+ const spawnedWorker = await spawnWorkerThread ( 'v2' ) ;
533+
534+ const handler = consumeAPI < PlaygroundCliBlueprintV2Worker > (
535+ spawnedWorker . phpPort
536+ ) ;
537+ handler . useFileLockManager ( fileLockManager as any ) ;
538+ await handler . bootWorker ( {
539+ siteUrl : options . siteUrl ,
540+ allow : options . allow ,
541+ phpVersion : options . phpVersion ,
542+ phpIniEntries : options . phpIniEntries ,
543+ constants : options . constants ,
544+ createFiles : options . createFiles ,
545+ firstProcessId : options . firstProcessId ,
546+ processIdSpaceLength : options . processIdSpaceLength ,
547+ trace : options . trace ,
548+ nativeInternalDirPath : options . nativeInternalDirPath ,
549+ withXdebug : options . withXdebug ,
550+ mountsBeforeWpInstall : [ ] ,
551+ mountsAfterWpInstall : [ ] ,
552+ } ) ;
553+
554+ return {
555+ php : handler ,
556+ reap : ( ) => {
557+ try {
558+ handler . dispose ( ) ;
559+ } catch {
560+ /** */
561+ }
562+ try {
563+ spawnedWorker . worker . terminate ( ) ;
564+ } catch {
565+ /** */
566+ }
567+ } ,
568+ } ;
569+ }
570+
493571process . on ( 'unhandledRejection' , ( e : any ) => {
494572 logger . error ( 'Unhandled rejection:' , e ) ;
495573} ) ;
0 commit comments