Skip to content

Commit

Permalink
Support proc_open(), exec(), passthru(), system()
Browse files Browse the repository at this point in the history
Provides a custom implementation of:

* php_exec – under the name wasm_php_exec
* proc_open

Both allow the C API to call a JavaScript callback set
via setSpawnHandler().
  • Loading branch information
adamziel committed Oct 24, 2023
1 parent b032a9c commit b4cfb8c
Show file tree
Hide file tree
Showing 35 changed files with 4,280 additions and 3,386 deletions.
2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
"prepublishOnly": "npm run build",
"preview": "nx preview playground-website",
"recompile:php:web": "nx recompile-php:all php-wasm-web",
"recompile:php:web:5.6": "nx recompile-php php-wasm-web --PHP_VERSION=5.6",
"recompile:php:web:7.0": "nx recompile-php php-wasm-web --PHP_VERSION=7.0",
"recompile:php:web:7.1": "nx recompile-php php-wasm-web --PHP_VERSION=7.1",
"recompile:php:web:7.2": "nx recompile-php php-wasm-web --PHP_VERSION=7.2",
Expand All @@ -25,7 +24,6 @@
"recompile:php:web:8.1": "nx recompile-php php-wasm-web --PHP_VERSION=8.1",
"recompile:php:web:8.2": "nx recompile-php php-wasm-web --PHP_VERSION=8.2",
"recompile:php:node": "nx recompile-php:all php-wasm-node",
"recompile:php:node:5.6": "nx recompile-php php-wasm-node --PHP_VERSION=5.6",
"recompile:php:node:7.0": "nx recompile-php php-wasm-node --PHP_VERSION=7.0",
"recompile:php:node:7.1": "nx recompile-php php-wasm-node --PHP_VERSION=7.1",
"recompile:php:node:7.2": "nx recompile-php php-wasm-node --PHP_VERSION=7.2",
Expand Down
57 changes: 50 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 } from 'fs';
import { writeFileSync, existsSync, mkdtempSync } from 'fs';
import { rootCertificates } from 'tls';

import {
Expand All @@ -11,6 +11,7 @@ import {
} from '@php-wasm/universal';

import { NodePHP } from '@php-wasm/node';
import { spawn } from 'child_process';

let args = process.argv.slice(2);
if (!args.length) {
Expand Down Expand Up @@ -40,16 +41,58 @@ const php = await NodePHP.load(phpVersion, {
},
},
});

php.useHostFilesystem();
php.setSpawnHandler((command: string) => {
const phpWasmCommand = `${process.argv[0]} ${process.execArgv.join(' ')} ${
process.argv[1]
}`;
// Naively replace the PHP binary with the PHP-WASM command
// @TODO: Don't process the command. Lean on the shell to do it, e.g. through
// a PATH or an alias.
const updatedCommand = command.replace(
/^(?:\\ |[^ ])*php\d?(\s|$)/,
phpWasmCommand
);
console.log({
phpWasmCommand,
command,
updatedCommand,
script: `#!/bin/sh
${updatedCommand} < /dev/stdin
`,
});

// Create a shell script in a temporary directory
const tempDir = mkdtempSync('php-wasm-');
const tempScriptPath = `${tempDir}/script.sh`;
writeFileSync(
tempScriptPath,
`#!/bin/sh
${updatedCommand} < /dev/stdin
`
);

return spawn('sh', [tempScriptPath], {
shell: true,
stdio: ['pipe', 'pipe', 'pipe'],
timeout: 100,
});
});

const hasMinusCOption = args.some((arg) => arg.startsWith('-c'));
if (!hasMinusCOption) {
args.unshift('-c', defaultPhpIniPath);
}

php.cli(['php', ...args]).catch((result) => {
if (result.name === 'ExitStatus') {
process.exit(result.status === undefined ? 1 : result.status);
}
throw result;
});
await php
.cli(['php', ...args])
.catch((result) => {
if (result.name === 'ExitStatus') {
process.exit(result.status === undefined ? 1 : result.status);
}
throw result;
})
.finally(() => {
process.exit(0);
});
1 change: 1 addition & 0 deletions packages/php-wasm/cli/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export default defineConfig(() => {
'net',
'fs',
'path',
'child_process',
'http',
'tls',
'util',
Expand Down
105 changes: 76 additions & 29 deletions packages/php-wasm/compile/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,12 @@ RUN source /root/emsdk/emsdk_env.sh && \
RUN echo '#define ZEND_MM_ERROR 0' >> /root/php-src/main/php_config.h;

# With HAVE_UNISTD_H=1 PHP complains about the missing getdtablesize() function
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 HAVE_UNISTD_H 1/define HAVE_UNISTD_H 0/g' /root/php-src/main/php_config.h
RUN /root/replace.sh 's/\(proc_open\)/(php_proc_open)/g' /root/php-src/ext/standard/proc_open.c
RUN /root/replace.sh 's/PHPAPI int php_exec(.+)$/PHPAPI extern int php_exec\1; int php_exec_old\1/g' /root/php-src/ext/standard/exec.c

# PHP <= 7.3 is not very good at detecting the presence of the POSIX readdir_r function
# so we need to force it to be enabled.
Expand All @@ -502,16 +507,22 @@ RUN /root/replace.sh 's/static int php_cli_server_poller_poll/extern int wasm_se

# 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
RUN /root/replace.sh 's/VCWD_POPEN\(/wasm_popen(/g' /root/php-src/ext/standard/file.c
RUN /root/replace.sh 's/PHP_FUNCTION\(popen\)/extern FILE *wasm_popen(const char *cmd, const char *mode);PHP_FUNCTION(popen)/g' /root/php-src/ext/standard/file.c

# Provide a custom implementation of the shutdown() function.
RUN perl -pi.bak -e $'s/(\s+)shutdown\(/$1 wasm_shutdown(/g' /root/php-src/sapi/cli/php_cli_server.c
RUN perl -pi.bak -e $'s/(\s+)closesocket\(/$1 wasm_close(/g' /root/php-src/sapi/cli/php_cli_server.c
RUN echo 'extern int wasm_shutdown(int fd, int how);' >> /root/php-src/main/php_config.h;
RUN echo 'extern int wasm_close(int fd);' >> /root/php-src/main/php_config.h;

# Provide a custom implementation of the proc_open() function.
RUN echo '' > /root/php-src/ext/standard/proc_open.h;
RUN echo '' > /root/php-src/ext/standard/proc_open.c;

RUN source /root/emsdk/emsdk_env.sh && \
# We're compiling PHP as emscripten's side module...
EMCC_FLAGS=" -sSIDE_MODULE -Dsetsockopt=wasm_setsockopt -Dpopen=wasm_popen -Dpclose=wasm_pclose " \
EMCC_FLAGS=" -sSIDE_MODULE -Dsetsockopt=wasm_setsockopt -Dphp_exec=wasm_php_exec " \
# ...which means we must skip all the libraries - they will be provided in the final linking step.
EMCC_SKIP="-lz -ledit -ldl -lncurses -lzip -lpng16 -lssl -lcrypto -lxml2 -lc -lm -lsqlite3 /root/lib/lib/libxml2.a /root/lib/lib/libsqlite3.so /root/lib/lib/libsqlite3.a /root/lib/lib/libpng16.so" \
emmake make -j8
Expand All @@ -520,6 +531,16 @@ RUN cp -v /root/php-src/.libs/libphp*.la /root/lib/libphp.la
RUN cp -v /root/php-src/.libs/libphp*.a /root/lib/libphp.a

COPY ./build-assets/php_wasm.c /root/
COPY ./build-assets/proc_open* /root/

RUN if [[ "${PHP_VERSION:0:1}" -le "7" && "${PHP_VERSION:2:1}" -le "3" ]]; then \
cp /root/proc_open7.0.c /root/proc_open.c; \
cp /root/proc_open7.0.h /root/proc_open.h; \
else \
cp /root/proc_open7.4.c /root/proc_open.c; \
cp /root/proc_open7.4.h /root/proc_open.h; \
fi


RUN if [ "$EMSCRIPTEN_ENVIRONMENT" = "node" ]; then \
# Add nodefs when building for node.js
Expand All @@ -543,30 +564,33 @@ RUN if [ "${PHP_VERSION:0:1}" -lt "8" ]; then \
# Add ws networking proxy support if needed
RUN if [ "$WITH_WS_NETWORKING_PROXY" = "yes" ]; \
then \
echo -n ' -lwebsocket.js -s ASYNCIFY=1 -s ASYNCIFY_IGNORE_INDIRECT=1 ' >> /root/.emcc-php-wasm-flags; \
# Emscripten supports yielding from sync functions to JavaScript event loop, but all
# the synchronous functions doing that must be explicitly listed here. This is an
# exhaustive list that was created by compiling PHP with ASYNCIFY, running code that
# uses networking, observing the error, and listing the missing functions.
#
# If you a get an error similar to the one below, you need to add all the function on
# the stack to the "ASYNCIFY_ONLY" list below (in this case, it's php_mysqlnd_net_open_tcp_or_unix_pub):
#
# RuntimeError: unreachable
# at php_mysqlnd_net_open_tcp_or_unix_pub (<anonymous>:wasm-function[9341]:0x5e42b8)
# at byn$fpcast-emu$php_mysqlnd_net_open_tcp_or_unix_pub (<anonymous>:wasm-function[17222]:0x7795e9)
# at php_mysqlnd_net_connect_ex_pub (<anonymous>:wasm-function[9338]:0x5e3f02)
#
# Node cuts the trace short by default so use the --stack-trace-limit=50 CLI flag
# to get the entire stack.
#
# -------
#
# Related: Any errors like Fatal error: Cannot redeclare function ...
# are caused by dispatching a PHP request while an execution is paused
# due to an async call – it means the same PHP files are loaded before
# the previous request, where they're already loaded, is concluded.
export ASYNCIFY_IMPORTS=$'["_dlopen_js",\n\
echo -n ' -lwebsocket.js ' >> /root/.emcc-php-wasm-flags; \
fi

RUN echo -n ' -s ASYNCIFY=1 -s ASYNCIFY_IGNORE_INDIRECT=1 ' >> /root/.emcc-php-wasm-flags; \
# Emscripten supports yielding from sync functions to JavaScript event loop, but all
# the synchronous functions doing that must be explicitly listed here. This is an
# exhaustive list that was created by compiling PHP with ASYNCIFY, running code that
# uses networking, observing the error, and listing the missing functions.
#
# If you a get an error similar to the one below, you need to add all the function on
# the stack to the "ASYNCIFY_ONLY" list below (in this case, it's php_mysqlnd_net_open_tcp_or_unix_pub):
#
# RuntimeError: unreachable
# at php_mysqlnd_net_open_tcp_or_unix_pub (<anonymous>:wasm-function[9341]:0x5e42b8)
# at byn$fpcast-emu$php_mysqlnd_net_open_tcp_or_unix_pub (<anonymous>:wasm-function[17222]:0x7795e9)
# at php_mysqlnd_net_connect_ex_pub (<anonymous>:wasm-function[9338]:0x5e3f02)
#
# Node cuts the trace short by default so use the --stack-trace-limit=50 CLI flag
# to get the entire stack.
#
# -------
#
# Related: Any errors like Fatal error: Cannot redeclare function ...
# are caused by dispatching a PHP request while an execution is paused
# due to an async call – it means the same PHP files are loaded before
# the previous request, where they're already loaded, is concluded.
export ASYNCIFY_IMPORTS=$'["_dlopen_js",\n\
"invoke_i",\n\
"invoke_ii",\n\
"invoke_iii",\n\
Expand All @@ -586,9 +610,11 @@ RUN if [ "$WITH_WS_NETWORKING_PROXY" = "yes" ]; \
"invoke_viiiiii",\n\
"invoke_viiiiiii",\n\
"invoke_viiiiiiiii",\n\
"js_open_process",\n\
"js_popen_to_file",\n\
"wasm_poll_socket",\n\
"wasm_shutdown"]'; \
echo -n " -s ASYNCIFY_IMPORTS=$ASYNCIFY_IMPORTS " | tr -d "\n" >> /root/.emcc-php-wasm-flags; \
echo -n " -s ASYNCIFY_IMPORTS=$ASYNCIFY_IMPORTS " | tr -d "\n" >> /root/.emcc-php-wasm-flags; \
export ASYNCIFY_ONLY_UNPREFIXED=$'"dynCall_dd",\
"dynCall_i",\
"dynCall_ii",\
Expand Down Expand Up @@ -620,7 +646,14 @@ RUN if [ "$WITH_WS_NETWORKING_PROXY" = "yes" ]; \
"dynCall_viiiii",\
"dynCall_viiiiiii",\
"dynCall_viiiiiiii",'; \
export ASYNCIFY_ONLY=$'"zif_array_filter",\
export ASYNCIFY_ONLY=$'"__fwritex",\
"zif_sleep",\
"zif_stream_get_contents",\
"php_stdiop_read",\
"fwrite",\
"zif_fwrite",\
"php_stdiop_write",\
"zif_array_filter",\
"zend_call_known_instance_method_with_2_params",\
"zend_fetch_dimension_address_read_R",\
"_zval_dtor_func_for_ptr",\
Expand Down Expand Up @@ -676,6 +709,8 @@ RUN if [ "$WITH_WS_NETWORKING_PROXY" = "yes" ]; \
"ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_TMPVAR_CONST_HANDLER",\
"ZEND_ISSET_ISEMPTY_PROP_OBJ_SPEC_TMPVAR_HANDLER",\
"cli",\
"wasm_sleep",\
"wasm_php_exec",\
"wasm_sapi_handle_request",\
"_call_user_function_ex",\
"_call_user_function_impl",\
Expand Down Expand Up @@ -761,6 +796,7 @@ RUN if [ "$WITH_WS_NETWORKING_PROXY" = "yes" ]; \
"php_cli_server_do_event_for_each_fd_callback",\
"php_cli_server_poller_poll",\
"php_cli_server_recv_event_read_request",\
"php_exec",\
"php_execute_script",\
"php_fsockopen_stream",\
"php_getimagesize_from_any",\
Expand Down Expand Up @@ -883,6 +919,15 @@ RUN if [ "$WITH_WS_NETWORKING_PROXY" = "yes" ]; \
"zif_mysqli_stmt_execute",\
"zif_mysqli_stmt_fetch",\
"zif_preg_replace_callback",\
"zif_popen",\
"php_exec_ex",\
"wasm_popen",\
"zif_wasm_popen",\
"zif_system",\
"zif_exec",\
"zif_passthru",\
"zif_shell_exec",\
"zif_proc_open",\
"zif_stream_socket_client",\
"zim_PDOStatement_execute",\
"zim_PDO___construct",\
Expand All @@ -903,8 +948,7 @@ RUN if [ "$WITH_WS_NETWORKING_PROXY" = "yes" ]; \
if [ "${PHP_VERSION:0:1}" -lt "8" ]; then \
export ASYNCIFY_ONLY="$ASYNCIFY_ONLY,"$(echo "$ASYNCIFY_ONLY" | sed -E $'s/"([a-zA-Z])/"byn$fpcast-emu$\\1/g'); \
fi; \
echo -n ' -s ASYNCIFY_ONLY=['$ASYNCIFY_ONLY_UNPREFIXED$ASYNCIFY_ONLY'] '| tr -d "\n" >> /root/.emcc-php-wasm-flags; \
fi;
echo -n ' -s ASYNCIFY_ONLY=['$ASYNCIFY_ONLY_UNPREFIXED$ASYNCIFY_ONLY'] '| tr -d "\n" >> /root/.emcc-php-wasm-flags;

# Build the final .wasm file
RUN mkdir /root/output
Expand All @@ -914,6 +958,8 @@ RUN source /root/emsdk/emsdk_env.sh && \
"_phpwasm_destroy_uploaded_files_hash", \n\
"_phpwasm_init_uploaded_files_hash", \n\
"_phpwasm_register_uploaded_file", \n\
"_emscripten_sleep", \n\
"_wasm_sleep", \n\
"_wasm_set_phpini_path", \n\
"_wasm_set_phpini_entries", \n\
"_wasm_add_SERVER_entry", \n\
Expand Down Expand Up @@ -953,6 +999,7 @@ RUN source /root/emsdk/emsdk_env.sh && \
-s INVOKE_RUN=0 \
-s EXIT_RUNTIME=1 \
/root/lib/libphp.a \
/root/proc_open.c \
/root/php_wasm.c \
$(cat /root/.emcc-php-wasm-sources) \
-s ENVIRONMENT=$EMSCRIPTEN_ENVIRONMENT \
Expand Down
Loading

0 comments on commit b4cfb8c

Please sign in to comment.