Skip to content

Commit adde29d

Browse files
committed
[CLI] Spawn OS processes for PHP subprocesses in Blueprint V2 worker
When PHP calls proc_open() to spawn a subprocess, each subprocess now runs in a separate Node.js worker thread. This ensures OS-level file locks via fcntl() and LockFileEx() properly conflict between parent and child processes, matching native PHP behavior. This is the V2 counterpart to the V1 worker change in PR #3001.
1 parent e6d58c8 commit adde29d

File tree

1 file changed

+81
-2
lines changed

1 file changed

+81
-2
lines changed

packages/playground/cli/src/blueprints-v2/worker-thread-v2.ts

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import path from 'path';
3333
import { rootCertificates } from 'tls';
3434
import { MessageChannel, type MessagePort, parentPort } from 'worker_threads';
3535
import { jspi } from 'wasm-feature-detect';
36-
import { type RunCLIArgs } from '../run-cli';
36+
import { type RunCLIArgs, spawnWorkerThread } from '../run-cli';
3737
import 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+
491570
process.on('unhandledRejection', (e: any) => {
492571
logger.error('Unhandled rejection:', e);
493572
});

0 commit comments

Comments
 (0)