Skip to content

Commit 94b91da

Browse files
committed
[PHP] Isomorphic, reusable spawn handler
Extracts sandboxedSpawnHandlerFactory() as a separate export from `@php-wasm/universal`. Every module can reuse it now instead of recreating new spawn handlers. ## Testing instructions Confirm the CI tests pass
1 parent 0e92aa6 commit 94b91da

File tree

5 files changed

+105
-3
lines changed

5 files changed

+105
-3
lines changed

packages/php-wasm/node/src/lib/node-fs-mount.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { MountHandler } from '@php-wasm/universal';
22

33
export function createNodeFsMountHandler(localPath: string): MountHandler {
4-
return async function (php, FS, vfsMountPoint) {
4+
return function (php, FS, vfsMountPoint) {
55
FS.mount(FS.filesystems['NODEFS'], { root: localPath }, vfsMountPoint);
66
return () => {
77
FS!.unmount(vfsMountPoint);

packages/php-wasm/universal/src/lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export {
7676

7777
export { isExitCode } from './is-exit-code';
7878
export { proxyFileSystem } from './proxy-file-system';
79+
export { sandboxedSpawnHandlerFactory } from './sandboxed-spawn-handler-factory';
7980

8081
export * from './api';
8182
export type { WithAPIState as WithIsReady } from './api';
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { createSpawnHandler } from '@php-wasm/util';
2+
import type { PHPProcessManager } from './php-process-manager';
3+
4+
/**
5+
* An isomorphic proc_open() handler that implements typical shell in TypeScript
6+
* without relying on a server runtime. It can be used in the browser and Node.js
7+
* alike whenever you need to spawn a PHP subprocess, query the terminal size, etc.
8+
* It is open for future expansion if more shell or busybox calls are needed, but
9+
* advanced shell features such as piping, stream redirection etc. are outside of
10+
* the scope of this minimal handler. If they become vital at any point, let's
11+
* explore bringing in an actual shell implementation or at least a proper command
12+
* parser.
13+
*/
14+
export function sandboxedSpawnHandlerFactory(
15+
processManager: PHPProcessManager
16+
) {
17+
return createSpawnHandler(async function (args, processApi, options) {
18+
processApi.notifySpawn();
19+
if (args[0] === 'exec') {
20+
args.shift();
21+
}
22+
23+
if (args[0].endsWith('.php') || args[0].endsWith('.phar')) {
24+
args.unshift('php');
25+
}
26+
27+
const binaryName = args[0].split('/').pop();
28+
29+
// Mock programs required by wp-cli:
30+
if (
31+
args[0] === '/usr/bin/env' &&
32+
args[1] === 'stty' &&
33+
args[2] === 'size'
34+
) {
35+
// These numbers are hardcoded because this
36+
// spawnHandler is transmitted as a string to
37+
// the PHP backend and has no access to local
38+
// scope. It would be nice to find a way to
39+
// transfer / proxy a live object instead.
40+
// @TODO: Do not hardcode this
41+
processApi.stdout(`18 140`);
42+
processApi.exit(0);
43+
} else if (binaryName === 'tput' && args[1] === 'cols') {
44+
processApi.stdout(`140`);
45+
processApi.exit(0);
46+
} else if (binaryName === 'less') {
47+
processApi.on('stdin', (data: Uint8Array) => {
48+
processApi.stdout(data);
49+
});
50+
processApi.flushStdin();
51+
processApi.exit(0);
52+
} else if (binaryName === 'php') {
53+
const { php, reap } = await processManager.acquirePHPInstance({
54+
considerPrimary: false,
55+
});
56+
57+
php.chdir(options.cwd as string);
58+
try {
59+
// Figure out more about setting env, putenv(), etc.
60+
const result = await php.cli(args, {
61+
env: {
62+
...options.env,
63+
SCRIPT_PATH: args[1],
64+
// Set SHELL_PIPE to 0 to ensure WP-CLI formats
65+
// the output as ASCII tables.
66+
// @see https://github.com/wp-cli/wp-cli/issues/1102
67+
SHELL_PIPE: '0',
68+
},
69+
});
70+
71+
result.stdout.pipeTo(
72+
new WritableStream({
73+
write(chunk) {
74+
processApi.stdout(chunk);
75+
},
76+
})
77+
);
78+
result.stderr.pipeTo(
79+
new WritableStream({
80+
write(chunk) {
81+
processApi.stderr(chunk);
82+
},
83+
})
84+
);
85+
processApi.exit(await result.exitCode);
86+
} catch (e) {
87+
// An exception here means the PHP runtime has crashed.
88+
processApi.exit(1);
89+
throw e;
90+
} finally {
91+
reap();
92+
}
93+
} else {
94+
processApi.exit(1);
95+
}
96+
});
97+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
consumeAPI,
88
consumeAPISync,
99
exposeAPI,
10+
sandboxedSpawnHandlerFactory,
1011
} from '@php-wasm/universal';
1112
import { sprintf } from '@php-wasm/util';
1213
import { bootWordPress } from '@wp-playground/wordpress';
@@ -186,6 +187,7 @@ export class PlaygroundCliWorker extends PHPWorker {
186187
},
187188
cookieStore: internalCookieStore ? undefined : false,
188189
dataSqlPath,
190+
spawnHandler: sandboxedSpawnHandlerFactory,
189191
});
190192
this.__internal_setRequestHandler(requestHandler);
191193

packages/playground/wordpress/src/boot.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
PHPRequestHandler,
1212
proxyFileSystem,
1313
rotatePHPRuntime,
14+
sandboxedSpawnHandlerFactory,
1415
setPhpIniEntries,
1516
withPHPIniValues,
1617
writeFiles,
@@ -190,6 +191,7 @@ export async function bootWordPress(options: BootOptions) {
190191
}
191192

192193
export async function bootRequestHandler(options: BootOptions) {
194+
const spawnHandler = options.spawnHandler ?? sandboxedSpawnHandlerFactory;
193195
async function createPhp(
194196
requestHandler: PHPRequestHandler,
195197
isPrimary: boolean
@@ -233,9 +235,9 @@ export async function bootRequestHandler(options: BootOptions) {
233235

234236
// Spawn handler is responsible for spawning processes for all the
235237
// `popen()`, `proc_open()` etc. calls.
236-
if (options.spawnHandler) {
238+
if (spawnHandler) {
237239
await php.setSpawnHandler(
238-
options.spawnHandler(requestHandler.processManager)
240+
spawnHandler(requestHandler.processManager)
239241
);
240242
}
241243

0 commit comments

Comments
 (0)