Skip to content

Commit

Permalink
PHP: Support blocking pipes
Browse files Browse the repository at this point in the history
Shims read(2) functionallity by providing an alternative read()
function called wasm_read() that gracefully handles blocking
pipes. This is neecessary because Emscripten does not support
blocking pipes and instead returns EWOULDBLOCK.

See:

* #951
* emscripten-core/emscripten#13214

 ## Testing instructions

Confirm the `proc_open()` tests pass on CI. This PR adjusts a few
of them to make sure the output is read without the tricky sleep()
call.
  • Loading branch information
adamziel committed Jan 17, 2024
1 parent 073e02c commit e95f401
Show file tree
Hide file tree
Showing 9 changed files with 454 additions and 309 deletions.
24 changes: 17 additions & 7 deletions packages/php-wasm/cli/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* A CLI script that runs PHP CLI via the WebAssembly build.
*/
import { writeFileSync, existsSync, mkdtempSync } from 'fs';
import { writeFileSync, existsSync, mkdtempSync, rmSync, rmdirSync } from 'fs';
import { rootCertificates } from 'tls';

import {
Expand Down Expand Up @@ -66,11 +66,17 @@ async function run() {
`
);

return spawn('sh', [tempScriptPath], {
shell: true,
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 100,
});
try {
return spawn(updatedCommand, [], {
shell: true,
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 5000,
});
} finally {
// Remove the temporary directory
rmSync(tempScriptPath);
rmdirSync(tempDir);
}
});

const hasMinusCOption = args.some((arg) => arg.startsWith('-c'));
Expand All @@ -87,7 +93,11 @@ async function run() {
throw result;
})
.finally(() => {
process.exit(0);
setTimeout(() => {
process.exit(0);
// 100 is an arbitrary number. It's there to give any child processes
// a chance to pass their output to JS before the main process exits.
}, 100);
});
}

Expand Down
20 changes: 14 additions & 6 deletions packages/php-wasm/compile/php/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -247,9 +247,6 @@ RUN echo '#define ZEND_MM_ERROR 0' >> /root/php-src/main/php_config.h;
RUN /root/replace.sh 's/define php_sleep sleep/define php_sleep wasm_sleep/g' /root/php-src/main/php.h
RUN echo 'extern unsigned int wasm_sleep(unsigned int time);' >> /root/php-src/main/php.h;

RUN /root/replace.sh 's/#define php_stream_flush\(stream\) _php_stream_flush/extern int wasm_php_stream_flush(php_stream *stream, int closing);\n#define php_stream_flush(stream) wasm_php_stream_flush/g' /root/php-src/main/php_streams.h
RUN /root/replace.sh 's/_php_stream_flush\(stream/wasm_php_stream_flush(stream/g' /root/php-src/main/streams/streams.c

RUN /root/replace.sh 's/define HAVE_UNISTD_H 1/define HAVE_UNISTD_H 0/g' /root/php-src/main/php_config.h

# PHP <= 7.3 is not very good at detecting the presence of the POSIX readdir_r function
Expand All @@ -262,6 +259,9 @@ RUN if [[ "${PHP_VERSION:0:1}" -le "7" && "${PHP_VERSION:2:1}" -le "3" ]]; then
RUN /root/replace.sh 's/static inline int php_pollfd_for\(/int php_pollfd_for(php_socket_t fd, int events, struct timeval *timeouttv); static inline int __real_php_pollfd_for(/g' /root/php-src/main/php_network.h
RUN /root/replace.sh 's/static int php_cli_server_poller_poll/extern int wasm_select(int, fd_set * __restrict, fd_set * __restrict, fd_set * __restrict, struct timeval * __restrict); static int php_cli_server_poller_poll/g' /root/php-src/sapi/cli/php_cli_server.c

RUN echo 'extern ssize_t wasm_read(int fd, void *buf, size_t count);' >> /root/php-src/main/php.h;
RUN /root/replace.sh 's/ret = read/ret = wasm_read/g' /root/php-src/main/streams/plain_wrapper.c

# Provide a custom implementation of the php_select() function.
RUN /root/replace.sh 's/return php_select\(/return wasm_select(/g' /root/php-src/sapi/cli/php_cli_server.c

Expand Down Expand Up @@ -394,8 +394,9 @@ RUN echo -n ' -s ASYNCIFY=1 -s ASYNCIFY_IGNORE_INDIRECT=1 ' >> /root/.emcc-php-w
"invoke_viiiiiiiii",\n\
"js_open_process",\n\
"js_popen_to_file",\n\
"wasm_poll_socket",\n\
"js_fd_read",\n\
"js_module_onMessage",\n\
"wasm_poll_socket",\n\
"wasm_shutdown"]'; \
echo -n " -s ASYNCIFY_IMPORTS=$ASYNCIFY_IMPORTS " | tr -d "\n" >> /root/.emcc-php-wasm-flags; \
export ASYNCIFY_ONLY_UNPREFIXED=$'"dynCall_dd",\
Expand Down Expand Up @@ -430,8 +431,14 @@ RUN echo -n ' -s ASYNCIFY=1 -s ASYNCIFY_IGNORE_INDIRECT=1 ' >> /root/.emcc-php-w
"dynCall_viiiiiii",\
"dynCall_viiiiiiii",'; \
export ASYNCIFY_ONLY=$'"__fwritex",\
"read",\
"zif_sleep",\
"zif_stream_get_contents",\
"_php_stream_fill_read_buffer",\
"_php_stream_read",\
"php_stream_read_to_str",\
"zif_fread",\
"wasm_read",\
"php_stdiop_read",\
"fwrite",\
"zif_fwrite",\
Expand All @@ -448,6 +455,8 @@ RUN echo -n ' -s ASYNCIFY=1 -s ASYNCIFY_IGNORE_INDIRECT=1 ' >> /root/.emcc-php-w
"php_stream_temp_flush",\
"_php_stream_flush",\
"php_stream_flush",\
"_php_stream_write",\
"php_stream_write",\
"_php_stream_free",\
"_php_stream_free_enclosed",\
"stream_resource_persistent_dtor",\
Expand Down Expand Up @@ -505,7 +514,6 @@ RUN echo -n ' -s ASYNCIFY=1 -s ASYNCIFY_IGNORE_INDIRECT=1 ' >> /root/.emcc-php-w
"wasm_sleep",\
"wasm_php_stream_flush",\
"wasm_php_stream_read",\
"_php_stream_read",\
"php_crc32_stream_bulk_update",\
"phar_parse_zipfile",\
"get_http_body",\
Expand Down Expand Up @@ -768,7 +776,7 @@ RUN set -euxo pipefail; \
"_wasm_set_phpini_path", \n\
"_wasm_set_phpini_entries", \n\
"_wasm_add_SERVER_entry", \n\
"_wasm_php_stream_flush", \n\
"_wasm_read", \n\
"_wasm_add_uploaded_file", \n\
"_wasm_sapi_handle_request", \n\
"_wasm_set_content_length", \n\
Expand Down
67 changes: 29 additions & 38 deletions packages/php-wasm/compile/php/php_wasm.c
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ unsigned int wasm_sleep(unsigned int time) {

extern int *wasm_setsockopt(int sockfd, int level, int optname, intptr_t optval, size_t optlen, int dummy);
extern char *js_popen_to_file(const char *cmd, const char *mode, uint8_t *exit_code_ptr);
extern int __wasi_syscall_ret(__wasi_errno_t code);
extern __wasi_errno_t js_fd_read(
__wasi_fd_t fd,
const __wasi_iovec_t* iovs,
size_t iovs_len,
__wasi_size_t* nread
);

/**
* Passes a message to the JavaScript module and writes the response
Expand Down Expand Up @@ -152,6 +159,28 @@ static size_t handle_line(int type, zval *array, char *buf, size_t bufl) {
return bufl;
}

/**
* Shims read(2) functionallity.
* Enables reading from blocking pipes. By default, Emscripten
* will throw an EWOULDBLOCK error when trying to read from a
* blocking pipe. This function overrides that behavior and
* instead waits for the pipe to become readable.
*
* @see https://github.com/WordPress/wordpress-playground/issues/951
* @see https://github.com/emscripten-core/emscripten/issues/13214
*/
EMSCRIPTEN_KEEPALIVE ssize_t wasm_read(int fd, void *buf, size_t count) {
struct __wasi_iovec_t iov = {
.buf = buf,
.buf_len = count
};
size_t num;
if (__wasi_syscall_ret(js_fd_read(fd, &iov, 1, &num))) {
return -1;
}
return num;
}

/*
* If type==0, only last line of output is returned (exec)
* If type==1, all lines will be printed and last lined returned (system)
Expand Down Expand Up @@ -250,44 +279,6 @@ EMSCRIPTEN_KEEPALIVE int wasm_php_exec(int type, const char *cmd, zval *array, z

// -----------------------------------------------------------

/**
* Replaces the default php_stream_read() implementation with our own that yields back to the JavaScript event
* loop to give JS a chance to actually populate the stream with any data that may already be buffered.
* This is the magic sauce that makes the stream_get_contents here work:
*
* $descriptorspec = array(
* 0 => array("pipe", "r"), // stdin
* 1 => array("pipe", "w"), // stdout
* 2 => array("pipe", "w") // stderr
* );
* $process = proc_open("less", $descriptorspec, $pipes);
* fwrite($pipes[0], "Hello world");
* fclose($pipes[0]);
* stream_get_contents($pipes[1]);
*
* Without the yield, stream_get_contents() would return an empty string because the data is not yet available.
* JavaScript populates the stream via asynchronous event handlers, so we need to yield back to give them a chance
* to run.
*/
int wasm_php_stream_flush(php_stream *stream, int closing)
{
int retval = _php_stream_flush(stream, closing);
// Yield to JS event loop. 500 is an arbitrary value that
// seems to work well in practice when writing to stdin
// via proc_open() as the underlying process catches up
// with the input. It is quite a long time, though, so
// this should be very much revisited.
//
// Unfortunately it's not a bulletproof solution and will
// likely break in some cases. A proper fix would be to
// identify which low level libc function behaves differently
// in emscripten and natively, and then patch it to yield back
// to the JS event loop – much like is being done with `select()`
// below.
emscripten_sleep(500);
return retval;
}

int wasm_socket_has_data(php_socket_t fd);
int wasm_poll_socket(php_socket_t fd, int events, int timeoutms);

Expand Down
55 changes: 55 additions & 0 deletions packages/php-wasm/compile/php/phpwasm-emscripten-library.js
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,61 @@ const LibraryExample = {
return 0;
},

/**
* Shims read(2) functionallity.
* Enables reading from blocking pipes. By default, Emscripten
* will throw an EWOULDBLOCK error when trying to read from a
* blocking pipe. This function overrides that behavior and
* instead waits for the pipe to become readable.
*
* @see https://github.com/WordPress/wordpress-playground/issues/951
* @see https://github.com/emscripten-core/emscripten/issues/13214
*/
js_fd_read: function (fd, iov, iovcnt, pnum) {
var returnCode;
try {
var stream = SYSCALLS.getStreamFromFD(fd);
var num = doReadv(stream, iov, iovcnt);
HEAPU32[pnum >> 2] = num;
returnCode = 0;
} catch (e) {
if (typeof FS == 'undefined' || !(e.name === 'ErrnoError')) throw e;
returnCode = e.errno;
}

if (returnCode === 6 /*EWOULDBLOCK*/) {
return Asyncify.handleSleep(function (wakeUp) {
var timeout = 10000; // @TODO Do not hardcode this
var interval = 50;
var retries = 0;
var maxRetries = timeout / interval;
var pollHandle = setInterval(function poll() {
var returnCode;
try {
var stream = SYSCALLS.getStreamFromFD(fd);
var num = doReadv(stream, iov, iovcnt);
HEAPU32[pnum >> 2] = num;
returnCode = 0;
} catch (e) {
if (
typeof FS == 'undefined' ||
!(e.name === 'ErrnoError')
) {
console.error(e);
throw e;
}
returnCode = e.errno;
}
if (returnCode !== 6 || ++retries > maxRetries) {
clearInterval(pollHandle);
wakeUp(returnCode);
}
}, interval);
});
}
return returnCode;
},

/**
* Shims popen(3) functionallity:
* https://man7.org/linux/man-pages/man3/popen.3.html
Expand Down
Binary file modified packages/php-wasm/node/public/8_1_23/php_8_1.wasm
Binary file not shown.
Binary file modified packages/php-wasm/node/public/8_2_10/php_8_2.wasm
Binary file not shown.
Loading

0 comments on commit e95f401

Please sign in to comment.