diff --git a/packages/php-wasm/node/src/lib/node-fs-mount.ts b/packages/php-wasm/node/src/lib/node-fs-mount.ts index 7f7ab08106..e75adeac0a 100644 --- a/packages/php-wasm/node/src/lib/node-fs-mount.ts +++ b/packages/php-wasm/node/src/lib/node-fs-mount.ts @@ -1,7 +1,7 @@ import type { MountHandler } from '@php-wasm/universal'; export function createNodeFsMountHandler(localPath: string): MountHandler { - return async function (php, FS, vfsMountPoint) { + return function (php, FS, vfsMountPoint) { FS.mount(FS.filesystems['NODEFS'], { root: localPath }, vfsMountPoint); return () => { FS!.unmount(vfsMountPoint); diff --git a/packages/php-wasm/universal/src/lib/index.ts b/packages/php-wasm/universal/src/lib/index.ts index 8fb02068b9..5ec07dcf71 100644 --- a/packages/php-wasm/universal/src/lib/index.ts +++ b/packages/php-wasm/universal/src/lib/index.ts @@ -76,6 +76,7 @@ export { export { isExitCode } from './is-exit-code'; export { proxyFileSystem } from './proxy-file-system'; +export { sandboxedSpawnHandlerFactory } from './sandboxed-spawn-handler-factory'; export * from './api'; export type { WithAPIState as WithIsReady } from './api'; diff --git a/packages/php-wasm/universal/src/lib/sandboxed-spawn-handler-factory.ts b/packages/php-wasm/universal/src/lib/sandboxed-spawn-handler-factory.ts new file mode 100644 index 0000000000..c61024337f --- /dev/null +++ b/packages/php-wasm/universal/src/lib/sandboxed-spawn-handler-factory.ts @@ -0,0 +1,96 @@ +import { createSpawnHandler } from '@php-wasm/util'; +import type { PHPProcessManager } from './php-process-manager'; + +/** + * An isomorphic proc_open() handler that implements typical shell in TypeScript + * without relying on a server runtime. It can be used in the browser and Node.js + * alike whenever you need to spawn a PHP subprocess, query the terminal size, etc. + * It is open for future expansion if more shell or busybox calls are needed, but + * advanced shell features such as piping, stream redirection etc. are outside of + * the scope of this minimal handler. If they become vital at any point, let's + * explore bringing in an actual shell implementation or at least a proper command + * parser. + */ +export function sandboxedSpawnHandlerFactory( + processManager: PHPProcessManager +) { + return createSpawnHandler(async function (args, processApi, options) { + processApi.notifySpawn(); + if (args[0] === 'exec') { + args.shift(); + } + + if (args[0].endsWith('.php') || args[0].endsWith('.phar')) { + args.unshift('php'); + } + + const binaryName = args[0].split('/').pop(); + + // Mock programs required by wp-cli: + if ( + args[0] === '/usr/bin/env' && + args[1] === 'stty' && + args[2] === 'size' + ) { + // These numbers are hardcoded because this + // spawnHandler is transmitted as a string to + // the PHP backend and has no access to local + // scope. It would be nice to find a way to + // transfer / proxy a live object instead. + // @TODO: Do not hardcode this + processApi.stdout(`18 140`); + processApi.exit(0); + } else if (binaryName === 'tput' && args[1] === 'cols') { + processApi.stdout(`140`); + processApi.exit(0); + } else if (binaryName === 'less') { + processApi.on('stdin', (data: Uint8Array) => { + processApi.stdout(data.buffer as ArrayBuffer); + }); + processApi.exit(0); + } 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', + }, + }); + + result.stdout.pipeTo( + new WritableStream({ + write(chunk) { + processApi.stdout(chunk.buffer as ArrayBuffer); + }, + }) + ); + result.stderr.pipeTo( + new WritableStream({ + write(chunk) { + processApi.stderr(chunk.buffer as ArrayBuffer); + }, + }) + ); + processApi.exit(await result.exitCode); + } catch (e) { + // An exception here means the PHP runtime has crashed. + processApi.exit(1); + throw e; + } finally { + reap(); + } + } else { + processApi.exit(1); + } + }); +} diff --git a/packages/playground/cli/src/worker-thread.ts b/packages/playground/cli/src/worker-thread.ts index 94e11003be..23ce53df41 100644 --- a/packages/playground/cli/src/worker-thread.ts +++ b/packages/playground/cli/src/worker-thread.ts @@ -7,6 +7,7 @@ import { consumeAPI, consumeAPISync, exposeAPI, + sandboxedSpawnHandlerFactory, } from '@php-wasm/universal'; import { sprintf } from '@php-wasm/util'; import { bootWordPress } from '@wp-playground/wordpress'; @@ -186,6 +187,7 @@ export class PlaygroundCliWorker extends PHPWorker { }, cookieStore: internalCookieStore ? undefined : false, dataSqlPath, + spawnHandler: sandboxedSpawnHandlerFactory, }); this.__internal_setRequestHandler(requestHandler); diff --git a/packages/playground/wordpress/src/boot.ts b/packages/playground/wordpress/src/boot.ts index 6cfa7826f2..e3c6df47b8 100644 --- a/packages/playground/wordpress/src/boot.ts +++ b/packages/playground/wordpress/src/boot.ts @@ -11,6 +11,7 @@ import { PHPRequestHandler, proxyFileSystem, rotatePHPRuntime, + sandboxedSpawnHandlerFactory, setPhpIniEntries, withPHPIniValues, writeFiles, @@ -190,6 +191,7 @@ export async function bootWordPress(options: BootOptions) { } export async function bootRequestHandler(options: BootOptions) { + const spawnHandler = options.spawnHandler ?? sandboxedSpawnHandlerFactory; async function createPhp( requestHandler: PHPRequestHandler, isPrimary: boolean @@ -233,9 +235,9 @@ export async function bootRequestHandler(options: BootOptions) { // Spawn handler is responsible for spawning processes for all the // `popen()`, `proc_open()` etc. calls. - if (options.spawnHandler) { + if (spawnHandler) { await php.setSpawnHandler( - options.spawnHandler(requestHandler.processManager) + spawnHandler(requestHandler.processManager) ); }