Skip to content

Prototype: Spawning PHP sub-processes in Web Workers #1031

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed

Conversation

adamziel
Copy link
Collaborator

@adamziel adamziel commented Feb 11, 2024

Related to #1026 and #1027

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 adds a spawnHandler that does the following:

  1. Creates another instance of WebPHP (child)
  2. Populates child's FS with parent's files
  3. Runs requested PHP script the child
  4. Syncs the child FS changes back to the parent

A shared filesystem didn't pan out. Synchronizing is the second best option.

This code snippet illustrates the idea – note the actual implementation is more nuanced:

php.setSpawnHandler(
	createSpawnHandler(async function (args, processApi) {
		const childPHP = new WebPHP();
		syncFS(php, childPHP);
		const { exitCode, stdout, stderr } = await childPHP.run({
			scriptPath: args[1]
		});
		syncFS(childPHP, php);
		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

/wordpress/spawn.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
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

Unit tests, E2E tests, and more testing instructions coming soon.

cc @dmsnell @bgrgicak

@dmsnell
Copy link
Member

dmsnell commented Feb 13, 2024

while normal environments share the FS across running PHP instances, I wonder if there would be a special reason to lock the FS during Playground bootup, in case something depends on having certain plugins installed or wants to read config values. I'm thinking about the case where a worker and main thread are both initializing at the same time. did you work through these cases?

adamziel added a commit that referenced this pull request Feb 27, 2024
Introduces a naive shell command parser to provide equally good support
for the following two types of `proc_open()` calls:

```php
proc_open([ "wp-cli.phar", "plugin", "install", "gutenberg" ]);
proc_open("wp-cli.phar plugin install gutenberg" ]);
```

The command parsing semantics are extremely naive at this point and only
cover splitting the command into an array of arguments as follows:

```ts
splitShellCommand(`wp option set      blogname "My \"fancy\" blog "'name'`);
> ["wp", "option", "set", "blogname", `my "fancy" blog name`]
```

There is no support for inline ENV variables, pipes, or redirects. For
those, we might need to build an actual shell binary OR turn to
something like [bun
shell](#1062).

## Testing instructions

This PR ships unit tests so just confirm the CI checks pass.

## Related resources

* #1031
* #1062
* #1051
@adamziel
Copy link
Collaborator Author

while normal environments share the FS across running PHP instances, I wonder if there would be a special reason to lock the FS during Playground bootup, in case something depends on having certain plugins installed or wants to read config values. I'm thinking about the case where a worker and main thread are both initializing at the same time. did you work through these cases?

Both PHP instances run in the same worker so there is no concurrency involved. Everything is happening sequentially. Assuming reliable two-way Filesystem sync, both PHP instances would have access to the same plugins and config values.

@adamziel
Copy link
Collaborator Author

Perhaps we don't need to sync filesystem changes after all. Emscripten provides PROXYFS that could potentially enable mounting one of the filesystems in the other, effectively making both PHP instances work with the same files.

childPHP = new WebPHP(await recreateRuntime(), {
documentRoot: DOCROOT,
absoluteUrl: scopedSiteUrl,
});
Copy link
Collaborator Author

@adamziel adamziel Feb 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's an alternative idea for FS syncing:

for ( const path of [DOCROOT, "/tmp"] ) {
	childPHP[__private__dont__use].FS.mount(childPHP[__private__dont__use].PROXYFS, {
		root: path,
		fs: php[__private__dont__use].FS
	}, path);
}

@adamziel
Copy link
Collaborator Author

Superseded by #1069

@adamziel adamziel closed this Feb 28, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants