Skip to content

Commit ac9bf28

Browse files
authored
[PHP] Isomorphic, reusable spawn handler (#2359)
Extracts `sandboxedSpawnHandlerFactory()` as a separate export from `@php-wasm/universal`. Every module can reuse it now instead of recreating new spawn handlers. Pre-requisite for #2281 ## Testing instructions Confirm the CI tests pass
1 parent 121a8bc commit ac9bf28

File tree

4 files changed

+103
-2
lines changed

4 files changed

+103
-2
lines changed

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

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

8181
export { isExitCode } from './is-exit-code';
8282
export { proxyFileSystem } from './proxy-file-system';
83+
export { sandboxedSpawnHandlerFactory } from './sandboxed-spawn-handler-factory';
8384

8485
export * from './api';
8586
export type { WithAPIState as WithIsReady } from './api';
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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.buffer as ArrayBuffer);
49+
});
50+
processApi.exit(0);
51+
} else if (binaryName === 'php') {
52+
const { php, reap } = await processManager.acquirePHPInstance({
53+
considerPrimary: false,
54+
});
55+
56+
php.chdir(options.cwd as string);
57+
try {
58+
// Figure out more about setting env, putenv(), etc.
59+
const result = await php.cli(args, {
60+
env: {
61+
...options.env,
62+
SCRIPT_PATH: args[1],
63+
// Set SHELL_PIPE to 0 to ensure WP-CLI formats
64+
// the output as ASCII tables.
65+
// @see https://github.com/wp-cli/wp-cli/issues/1102
66+
SHELL_PIPE: '0',
67+
},
68+
});
69+
70+
result.stdout.pipeTo(
71+
new WritableStream({
72+
write(chunk) {
73+
processApi.stdout(chunk.buffer as ArrayBuffer);
74+
},
75+
})
76+
);
77+
result.stderr.pipeTo(
78+
new WritableStream({
79+
write(chunk) {
80+
processApi.stderr(chunk.buffer as ArrayBuffer);
81+
},
82+
})
83+
);
84+
processApi.exit(await result.exitCode);
85+
} catch (e) {
86+
// An exception here means the PHP runtime has crashed.
87+
processApi.exit(1);
88+
throw e;
89+
} finally {
90+
reap();
91+
}
92+
} else {
93+
processApi.exit(1);
94+
}
95+
});
96+
}

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)