Skip to content

Commit bd9139a

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 1244226 commit bd9139a

File tree

1 file changed

+81
-3
lines changed

1 file changed

+81
-3
lines changed

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

Lines changed: 81 additions & 3 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,
@@ -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+
493571
process.on('unhandledRejection', (e: any) => {
494572
logger.error('Unhandled rejection:', e);
495573
});

0 commit comments

Comments
 (0)