diff --git a/packages/php-wasm/node/src/test/php.spec.ts b/packages/php-wasm/node/src/test/php.spec.ts index 75638fd6bc..cab2562260 100644 --- a/packages/php-wasm/node/src/test/php.spec.ts +++ b/packages/php-wasm/node/src/test/php.spec.ts @@ -3,7 +3,10 @@ import { getPhpIniEntries, loadPHPRuntime, PHP, + PHPProcessManager, + sandboxedSpawnHandlerFactory, setPhpIniEntries, + type SpawnedPHP, SupportedPHPVersions, } from '@php-wasm/universal'; import { createSpawnHandler, joinPaths, phpVar } from '@php-wasm/util'; @@ -21,6 +24,7 @@ import { vi } from 'vitest'; import { getPHPLoaderModule, loadNodeRuntime } from '..'; import type { PHPLoaderOptions } from '..'; import { createNodeFsMountHandler } from '../lib/node-fs-mount'; +import { RecommendedPHPVersion } from '@wp-playground/common'; const testDirPath = '/__test987654321'; const testFilePath = '/__test987654321.txt'; @@ -2978,3 +2982,75 @@ phpLoaderOptions.forEach((options) => { }); }); }); + +describe('sandboxedSpawnHandlerFactory', () => { + const phpVersion = RecommendedPHPVersion; + let php: PHP; + let spawnedPhp: SpawnedPHP; + let processManager: PHPProcessManager; + beforeEach(async () => { + processManager = new PHPProcessManager({ + phpFactory: async () => { + const php = new PHP( + await loadNodeRuntime(phpVersion as any, {}) + ); + php.mkdir('/tmp/shared-test-directory'); + php.chdir('/tmp/shared-test-directory'); + + php.writeFile( + '/tmp/shared-test-directory/README.md', + 'Hello, world!' + ); + php.mkdir('/tmp/shared-test-directory/code'); + php.writeFile( + '/tmp/shared-test-directory/code/index.php', + 'Hello, world!' + ); + await php.setSpawnHandler( + sandboxedSpawnHandlerFactory(processManager) + ); + return php; + }, + maxPhpInstances: 5, + }); + spawnedPhp = await processManager.acquirePHPInstance(); + php = spawnedPhp.php; + }); + afterEach(async () => { + await processManager[Symbol.asyncDispose](); + spawnedPhp?.reap(); + }); + it.each([ + // Default cwd + { + command: 'ls', + expected: ['README.md', 'code', ''].join('\n'), + }, + // Explicit path + { + command: 'ls /tmp/shared-test-directory', + expected: ['README.md', 'code', ''].join('\n'), + }, + // Subdirectory – we expect a different output + { + command: 'ls /tmp/shared-test-directory/code', + expected: ['index.php', ''].join('\n'), + }, + // pwd + { + command: 'pwd', + expected: '/tmp/shared-test-directory\n', + }, + ])('should be able to run "$command"', async ({ command, expected }) => { + const response = await php.run({ + code: ` { + processApi.on('stdin', (data) => { processApi.stdout(data); }); // Exit after the stdin stream is exhausted. @@ -55,49 +55,83 @@ export function sandboxedSpawnHandlerFactory( }); processApi.exit(0); return; - } else if (binaryName === 'php') { - const { php, reap } = await processManager.acquirePHPInstance({ - considerPrimary: false, - }); + } - php.chdir(options.cwd as string); - try { - // Figure out more about setting env, putenv(), etc. - const result = await php.cli(args, { - env: { - ...options.env, - SCRIPT_PATH: args[1], - // Set SHELL_PIPE to 0 to ensure WP-CLI formats - // the output as ASCII tables. - // @see https://github.com/wp-cli/wp-cli/issues/1102 - SHELL_PIPE: '0', - }, - }); + if (!['php', 'ls', 'pwd'].includes(binaryName ?? '')) { + // 127 is the exit code "for command not found". + processApi.exit(127); + return; + } - result.stdout.pipeTo( - new WritableStream({ - write(chunk) { - processApi.stdout(chunk); - }, - }) - ); - result.stderr.pipeTo( - new WritableStream({ - write(chunk) { - processApi.stderr(chunk); + const { php, reap } = await processManager.acquirePHPInstance({ + considerPrimary: false, + }); + + try { + if (options.cwd) { + php.chdir(options.cwd as string); + } + + const cwd = php.cwd(); + switch (binaryName) { + case 'php': { + // Figure out more about setting env, putenv(), etc. + const result = await php.cli(args, { + env: { + ...options.env, + SCRIPT_PATH: args[1], + // Set SHELL_PIPE to 0 to ensure WP-CLI formats + // the output as ASCII tables. + // @see https://github.com/wp-cli/wp-cli/issues/1102 + SHELL_PIPE: '0', }, - }) - ); - processApi.exit(await result.exitCode); - } catch (e) { - // An exception here means the PHP runtime has crashed. - processApi.exit(1); - throw e; - } finally { - reap(); + }); + + result.stdout.pipeTo( + new WritableStream({ + write(chunk) { + processApi.stdout(chunk as any as ArrayBuffer); + }, + }) + ); + result.stderr.pipeTo( + new WritableStream({ + write(chunk) { + processApi.stderr(chunk as any as ArrayBuffer); + }, + }) + ); + processApi.exit(await result.exitCode); + break; + } + case 'ls': { + const files = php.listFiles(args[1] ?? cwd); + for (const file of files) { + processApi.stdout(file + '\n'); + } + // Technical limitation of subprocesses – we need to + // wait before exiting to give consumer a chance to read + // the output. + await new Promise((resolve) => setTimeout(resolve, 10)); + processApi.exit(0); + break; + } + case 'pwd': { + processApi.stdout(cwd + '\n'); + // Technical limitation of subprocesses – we need to + // wait before exiting to give consumer a chance to read + // the output. + await new Promise((resolve) => setTimeout(resolve, 10)); + processApi.exit(0); + break; + } } - } else { + } catch (e) { + // An exception here means the PHP runtime has crashed. processApi.exit(1); + throw e; + } finally { + reap(); } }); }