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