From f64c88a064988e78d99707c058302a52b4c1b7bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 30 Sep 2025 15:49:30 +0200 Subject: [PATCH 1/2] Preserve chroot across all worker-managed PHP instances --- packages/php-wasm/node/project.json | 6 ++- .../php-wasm/node/src/test/php-worker.spec.ts | 53 +++++++++++++++++++ .../php-wasm/universal/src/lib/php-worker.ts | 31 ++++++++--- 3 files changed, 82 insertions(+), 8 deletions(-) create mode 100644 packages/php-wasm/node/src/test/php-worker.spec.ts diff --git a/packages/php-wasm/node/project.json b/packages/php-wasm/node/project.json index 622deac767..e74fcfdb19 100644 --- a/packages/php-wasm/node/project.json +++ b/packages/php-wasm/node/project.json @@ -165,7 +165,8 @@ "php-dynamic-loading.spec.ts", "php-request-handler.spec.ts", "php.spec.ts", - "file-lock-manager-for-node.spec.ts" + "file-lock-manager-for-node.spec.ts", + "php-worker.spec.ts" ] } }, @@ -189,7 +190,8 @@ "php-dynamic-loading.spec.ts", "php-request-handler.spec.ts", "php.spec.ts", - "file-lock-manager-for-node.spec.ts" + "file-lock-manager-for-node.spec.ts", + "php-worker.spec.ts" ] } }, diff --git a/packages/php-wasm/node/src/test/php-worker.spec.ts b/packages/php-wasm/node/src/test/php-worker.spec.ts new file mode 100644 index 0000000000..b287b6a67e --- /dev/null +++ b/packages/php-wasm/node/src/test/php-worker.spec.ts @@ -0,0 +1,53 @@ +// eslint-disable-next-line @nx/enforce-module-boundaries -- ignore test-related interdependencies so we can test. +import { PHP, PHPRequestHandler, PHPWorker } from '@php-wasm/universal'; +import { loadNodeRuntime } from '..'; +import { RecommendedPHPVersion } from '@wp-playground/common'; + +describe('PHP Worker', () => { + let handler: PHPRequestHandler; + let worker: PHPWorker; + beforeEach(async () => { + handler = new PHPRequestHandler({ + documentRoot: '/wordpress', + absoluteUrl: 'http://127.0.0.1:2398', + phpFactory: async () => + new PHP(await loadNodeRuntime(RecommendedPHPVersion)), + maxPhpInstances: 3, + }); + worker = new PHPWorker(handler); + await worker.setPrimaryPHP(await handler.getPrimaryPhp()); + }); + + afterEach(async () => { + await handler[Symbol.asyncDispose](); + await worker[Symbol.asyncDispose](); + }); + + it('chdir() should change cwd for the worker', async () => { + worker.chdir('/tmp'); + expect(worker.cwd()).toBe('/tmp'); + }); + + it.each(['primary', 'non-primary'])( + 'chdir() should change cwd for the %s PHP instances', + async (instanceType) => { + worker.chdir('/tmp'); + + /** + * Block the primary PHP instance to ensure run() + * creates a fresh PHP instance. + */ + const { reap } = await handler.processManager.acquirePHPInstance({ + considerPrimary: instanceType === 'primary', + }); + try { + const response = await worker.run({ + code: `> = new Map(); onMessageListeners: MessageListener[] = []; @@ -101,6 +103,7 @@ export class PHPWorker implements LimitedPHPApi, AsyncDisposable { public __internal_setRequestHandler(requestHandler: PHPRequestHandler) { this.absoluteUrl = requestHandler.absoluteUrl; this.documentRoot = requestHandler.documentRoot; + this.chroot = this.documentRoot; _private.set(this, { ..._private.get(this), requestHandler, @@ -177,9 +180,7 @@ export class PHPWorker implements LimitedPHPApi, AsyncDisposable { /** @inheritDoc @php-wasm/universal!/PHP.run */ async run(request: PHPRunOptions): Promise { - const { php, reap } = await _private - .get(this)! - .requestHandler!.processManager.acquirePHPInstance(); + const { php, reap } = await this.acquirePHPInstance(); try { return await php.run(request); } finally { @@ -192,9 +193,7 @@ export class PHPWorker implements LimitedPHPApi, AsyncDisposable { argv: string[], options?: { env?: Record } ): Promise { - const { php, reap } = await _private - .get(this)! - .requestHandler!.processManager.acquirePHPInstance(); + const { php, reap } = await this.acquirePHPInstance(); try { return await php.cli(argv, options); } finally { @@ -204,9 +203,29 @@ export class PHPWorker implements LimitedPHPApi, AsyncDisposable { /** @inheritDoc @php-wasm/universal!/PHP.chdir */ chdir(path: string): void { + // Remember the new chroot for all PHP instances yet to be acquired. + this.chroot = path; return _private.get(this)!.php!.chdir(path); } + /** @inheritDoc @php-wasm/universal!/PHP.chdir */ + cwd(): string { + return _private.get(this)!.php!.cwd(); + } + + /** + * @returns A PHP instance with a consistent chroot. + */ + private async acquirePHPInstance() { + const { php, reap } = await _private + .get(this)! + .requestHandler!.processManager.acquirePHPInstance(); + if (this.chroot !== null) { + php.chdir(this.chroot); + } + return { php, reap }; + } + /** @inheritDoc @php-wasm/universal!/PHP.setSapiName */ setSapiName(newName: string): void { _private.get(this)!.php!.setSapiName(newName); From efda55028166471608cda01bc6022c34ba52215e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 30 Sep 2025 16:01:16 +0200 Subject: [PATCH 2/2] [PHP] Move registerWorkerListeners() call to acquireInstance in the PHPWorker class --- packages/php-wasm/node/src/test/php-worker.spec.ts | 11 +++++++++++ packages/php-wasm/universal/src/lib/php-worker.ts | 1 + .../cli/src/blueprints-v1/worker-thread-v1.ts | 1 - .../cli/src/blueprints-v2/worker-thread-v2.ts | 1 - .../remote/src/lib/playground-worker-endpoint.ts | 1 - 5 files changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/php-wasm/node/src/test/php-worker.spec.ts b/packages/php-wasm/node/src/test/php-worker.spec.ts index b287b6a67e..69e18e9913 100644 --- a/packages/php-wasm/node/src/test/php-worker.spec.ts +++ b/packages/php-wasm/node/src/test/php-worker.spec.ts @@ -50,4 +50,15 @@ describe('PHP Worker', () => { } } ); + + it('addEventListener() should add a listener for all PHP instances spawned by the worker', async () => { + const received: any[] = []; + worker.addEventListener('runtime.beforeExit', (event) => { + received.push(event); + }); + await worker.run({ + code: ` { - this.registerWorkerListeners(php); await mountResources(php, mountsBeforeWpInstall); await mountResources(php, mountsAfterWpInstall); }, diff --git a/packages/playground/cli/src/blueprints-v2/worker-thread-v2.ts b/packages/playground/cli/src/blueprints-v2/worker-thread-v2.ts index 8c6e0a3db7..1e9b81ab2d 100644 --- a/packages/playground/cli/src/blueprints-v2/worker-thread-v2.ts +++ b/packages/playground/cli/src/blueprints-v2/worker-thread-v2.ts @@ -220,7 +220,6 @@ export class PlaygroundCliBlueprintV2Worker extends PHPWorker { 'openssl.cafile': '/internal/shared/ca-bundle.crt', }, onPHPInstanceCreated: async (php: PHP) => { - this.registerWorkerListeners(php); await mountResources(php, args['mount-before-install'] || []); if (this.blueprintTargetResolved) { await mountResources(php, args.mount || []); diff --git a/packages/playground/remote/src/lib/playground-worker-endpoint.ts b/packages/playground/remote/src/lib/playground-worker-endpoint.ts index 41ca3133ab..6a5282662d 100644 --- a/packages/playground/remote/src/lib/playground-worker-endpoint.ts +++ b/packages/playground/remote/src/lib/playground-worker-endpoint.ts @@ -244,7 +244,6 @@ export abstract class PlaygroundWorkerEndpoint extends PHPWorker { if (withNetworking) { await this.networkTransport!.setupMessageHandler(php); } - this.registerWorkerListeners(php); }, spawnHandler: sandboxedSpawnHandlerFactory, sapiName,