WordPress/wordpress-playground

PHP: Support proc_open

adamziel opened this issue · 2 comments

PHPUnit and wp-cli both use proc_open and will only work correctly in WordPress Playground if that function is supported.

Today proc_open does not work becuse internally it calls fork() which is not supported in Emscripten and doesn't seem possible to polyfill.

Therefore, the only way to support proc_open is to shim it using require('child_process').spawn() similarly to how we shim popen():

js_popen_to_file: function(command, mode, exitCodePtr) {

const cp = require('child_process');
let ret;
if (modestr === 'r') {
ret = cp.spawnSync(cmdstr, [], {
shell: true,
stdio: ["inherit", "pipe", "inherit"],
});
HEAPU8[exitCodePtr] = ret.status;
require('fs').writeFileSync(pipeFilePath, ret.stdout, {
encoding: 'utf8',
flag: 'w+',
});
} else if (modestr === 'w') {

proc_open has the following signature in PHP:

proc_open(
    array|string $command,
    array $descriptor_spec,
    array &$pipes,
    ?string $cwd = null,
    ?array $env_vars = null,
    ?array $options = null
): resource|false

Handling $pipes is the most challenging bit here. Emscripten documentation doesn't explain how to create pipes – the easiest way should be reading the code and looking up examples.

If we can find a way to open Emscripten pipes and pass the data from/to the child process, that's great – that would probably involve custom Emscripten streams or devices. Otherwise, we could fake pipes by opening three temporary files in Emscripten (for stdin, stdout, stderr) and creating $pipes using those file descriptor.

cc @wojtekn @sejas @danielbachhuber @schlessera

PHPUnit uses proc_open to support process isolation (which WordPress relies on):

TestCase::run():

        if (!$this->shouldRunInSeparateProcess()) {
            (new TestRunner)->run($this);
        } else {
            (new TestRunner)->runInSeparateProcess(
                $this,
                $this->runClassInSeparateProcess && !$this->runTestInSeparateProcess,
                $this->preserveGlobalState,
            );
        }
    }

TestRunner::runInSeparateProcess:

$php = AbstractPhpProcess::factory();
$php->runTestJob($template->render(), $test);

DefaultPhpProcess::runProcess():

        $process = proc_open(
            $this->getCommand($settings, $this->tempFile),
            $pipeSpec,
            $pipes,
            null,
            $env,
        );