Skip to content

Commit 24f2c61

Browse files
authored
Spawning PHP sub-processes in Web Workers (#1069)
Adds support for spawning PHP subprocesses via `<?php proc_open(['php', 'activate_theme.php']);`. The spawned subprocess affects the filesystem used by the parent process. ## Implementation details This PR updates the default `spawnHandler` in `worker-thread.ts` that creates another WebPHP instance and mounts the parent filesystem using Emscripten's PROXYFS. [A shared filesystem didn't pan out. Synchronizing is the second best option.](#1027) This code snippet illustrates the idea – note the actual implementation is more nuanced: ```ts php.setSpawnHandler( createSpawnHandler(async function (args, processApi) { const childPHP = new WebPHP(); const { exitCode, stdout, stderr } = await childPHP.run({ scriptPath: args[1] }); processApi.stdout(stdout); processApi.stderr(stderr); processApi.exit(exitCode); }) ); ``` ## Future work * Stream `stdout` and `stderr` from `childPHP` to `processApi` instead of buffering the output and passing everything at once ## Example of how it works <img width="500" src="https://github.com/WordPress/wordpress-playground/assets/205419/470d79b2-2f10-4f1a-806c-5f26463766da" /> #### /wordpress/spawn.php ```php <?php echo "<plaintext>"; echo "Spawning /wordpress/child.php\n"; $handle = proc_open('php /wordpress/child.php', [ 0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w'], ], $pipes); echo "stdout: " . stream_get_contents($pipes[1]) . "\n"; echo "stderr: " . stream_get_contents($pipes[2]) . "\n"; echo "Finished\n"; echo "Contents of the created file: " . file_get_contents("/wordpress/new.txt") . "\n"; ``` #### /wordpress/child.php ```php <?php echo "<plaintext>"; echo "Spawned, running"; error_log("Here's a message logged to stderr! " . rand()); file_put_contents("/wordpress/new.txt", "Hello, world!" . rand() . "\n"); ``` ## Testing instructions 1. Update `worker-thread.ts` to create the two files listed above 2. In Playground, navigate to `/spawn.php` 3. Confirm the output is the same as on the screenshot above
1 parent 0eba044 commit 24f2c61

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+2120
-94
lines changed

packages/php-wasm/compile/php/Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -812,6 +812,7 @@ RUN set -euxo pipefail; \
812812
-I TSRM/ \
813813
-I /root/lib/include \
814814
-L/root/lib -L/root/lib/lib/ \
815+
-lproxyfs.js \
815816
$(cat /root/.emcc-php-wasm-flags) \
816817
-o /build/output/php.js \
817818
-s EXPORTED_FUNCTIONS="$EXPORTED_FUNCTIONS" \

packages/php-wasm/compile/php/php_wasm.c

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -954,7 +954,7 @@ void wasm_set_request_port(int port)
954954
*
955955
* stream: The stream to redirect, e.g. stdout or stderr.
956956
*
957-
* path: The path to the file to redirect to, e.g. "/tmp/stdout".
957+
* path: The path to the file to redirect to, e.g. "/internal/stdout".
958958
*
959959
* returns: The exit code: 0 on success, -1 on failure.
960960
*/
@@ -1175,8 +1175,13 @@ int wasm_sapi_request_init()
11751175
// Write to files instead of stdout and stderr because Emscripten truncates null
11761176
// bytes from stdout and stderr, and null bytes are a valid output when streaming
11771177
// binary data.
1178-
stdout_replacement = redirect_stream_to_file(stdout, "/tmp/stdout");
1179-
stderr_replacement = redirect_stream_to_file(stderr, "/tmp/stderr");
1178+
// We'll use the /internal directory instead of /tmp, because a child process sharing
1179+
// the same filesystem and /tmp mount would write to the same stdout and stderr files
1180+
// and produce unreadable output intertwined with the parent process output. The /internal
1181+
// directory should always stay in per-process MEMFS space and never be shared with
1182+
// any other process.
1183+
stdout_replacement = redirect_stream_to_file(stdout, "/internal/stdout");
1184+
stderr_replacement = redirect_stream_to_file(stderr, "/internal/stderr");
11801185
if (stdout_replacement == -1 || stderr_replacement == -1)
11811186
{
11821187
return -1;
@@ -1434,7 +1439,7 @@ FILE *headers_file;
14341439
*/
14351440
static int wasm_sapi_send_headers(sapi_headers_struct *sapi_headers TSRMLS_DC)
14361441
{
1437-
headers_file = fopen("/tmp/headers.json", "w");
1442+
headers_file = fopen("/internal/headers.json", "w");
14381443
if (headers_file == NULL)
14391444
{
14401445
return FAILURE;

packages/php-wasm/compile/php/phpwasm-emscripten-library.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ const LibraryExample = {
1616
// JavaScript library under the PHPWASM object:
1717
$PHPWASM: {
1818
init: function () {
19+
// The /internal directory is required by the C module. It's where the
20+
// stdout, stderr, and headers information are written for the JavaScript
21+
// code to read later on.
22+
FS.mkdir("/internal");
23+
1924
PHPWASM.EventEmitter = ENVIRONMENT_IS_NODE
2025
? require('events').EventEmitter
2126
: class EventEmitter {
21 Bytes
Binary file not shown.
-11 Bytes
Binary file not shown.
28 Bytes
Binary file not shown.
5 Bytes
Binary file not shown.
39 Bytes
Binary file not shown.
35 Bytes
Binary file not shown.
0 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)