Skip to content

Support proc_open(), exec(), passthru(), system() #596

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

Merged
merged 17 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 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:light": "nx recompile-php:light php-wasm-web ",
"recompile:php:web:light:5.6": "nx recompile-php:light php-wasm-web --PHP_VERSION=5.6",
"recompile:php:web:light:7.0": "nx recompile-php:light php-wasm-web --PHP_VERSION=7.0",
"recompile:php:web:light:7.1": "nx recompile-php:light php-wasm-web --PHP_VERSION=7.1",
"recompile:php:web:light:7.2": "nx recompile-php:light php-wasm-web --PHP_VERSION=7.2",
Expand All @@ -25,7 +24,6 @@
"recompile:php:web:light:8.1": "nx recompile-php:light php-wasm-web --PHP_VERSION=8.1",
"recompile:php:web:light:8.2": "nx recompile-php:light php-wasm-web --PHP_VERSION=8.2",
"recompile:php:web:kitchen-sink": "nx recompile-php:kitchen-sink php-wasm-web",
"recompile:php:web:kitchen-sink:5.6": "nx recompile-php:kitchen-sink php-wasm-web --PHP_VERSION=5.6",
"recompile:php:web:kitchen-sink:7.0": "nx recompile-php:kitchen-sink php-wasm-web --PHP_VERSION=7.0",
"recompile:php:web:kitchen-sink:7.1": "nx recompile-php:kitchen-sink php-wasm-web --PHP_VERSION=7.1",
"recompile:php:web:kitchen-sink:7.2": "nx recompile-php:kitchen-sink php-wasm-web --PHP_VERSION=7.2",
Expand All @@ -35,7 +33,6 @@
"recompile:php:web:kitchen-sink:8.1": "nx recompile-php:kitchen-sink php-wasm-web --PHP_VERSION=8.1",
"recompile:php:web:kitchen-sink:8.2": "nx recompile-php:kitchen-sink 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
49 changes: 42 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,50 @@ 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
);

// 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
126 changes: 89 additions & 37 deletions packages/php-wasm/compile/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ FROM emscripten AS emscripten-libzip
ARG PHP_VERSION
COPY --from=emscripten-libz /root/lib /root/lib-libz
RUN /root/copy-lib.sh lib-libz
RUN if [[ "${PHP_VERSION:0:1}" -le "7" && "${PHP_VERSION:2:1}" -le "3" ]] || [ "${PHP_VERSION:0:1}" -le "5" ]; then \
RUN if [[ "${PHP_VERSION:0:1}" -le "7" && "${PHP_VERSION:2:1}" -le "3" ]]; then \
export LIBZIP_VERSION=1.2.0; \
else \
export LIBZIP_VERSION=1.9.2; \
Expand Down Expand Up @@ -334,10 +334,7 @@ COPY --from=emscripten-libzip /root/lib /root/lib-libzip
RUN if [ "$WITH_LIBZIP" = "yes" ]; then \
/root/copy-lib.sh lib-libz; \
/root/copy-lib.sh lib-libzip && \
if [ "${PHP_VERSION:0:1}" -le "5" ]; then \
/root/replace.sh 's/ZEND_MODULE_GLOBALS_CTOR_N/(void (*)(void *))ZEND_MODULE_GLOBALS_CTOR_N/g' /root/php-src/ext/zlib/zlib.c; \
fi;\
if [[ "${PHP_VERSION:0:1}" -le "7" && "${PHP_VERSION:2:1}" -le "3" ]] || [ "${PHP_VERSION:0:1}" -le "5" ]; then \
if [[ "${PHP_VERSION:0:1}" -le "7" && "${PHP_VERSION:2:1}" -le "3" ]]; then \
apt install -y zlib1g zlib1g-dev; \
# https://php-legacy-docs.zend.com/manual/php5/en/zlib.installation
echo -n ' --with-zlib --with-zlib-dir=/root/lib --enable-zip --with-libzip=/root/lib ' >> /root/.php-configure-flags; \
Expand Down Expand Up @@ -390,7 +387,7 @@ RUN if [ "$WITH_LIBXML" = "yes" ]; \
# In the regular cc it's just a warning, but in the emscripten's emcc that's an error:
perl -pi.bak -e 's/char xmlInitParser/void xmlInitParser/g' /root/php-src/configure; \
# On PHP < 7.1.0, the dom_iterators.c file implicitly converts *char to const *char causing emcc error
if [[ "${PHP_VERSION:0:1}" -le "7" && "${PHP_VERSION:2:1}" -le "0" ]] || [ "${PHP_VERSION:0:1}" -le "5" ]; then \
if [[ "${PHP_VERSION:0:1}" -le "7" && "${PHP_VERSION:2:1}" -le "0" ]]; then \
/root/replace.sh 's/xmlHashScan\(ht, itemHashScanner, iter\);/xmlHashScan(ht, (xmlHashScanner)itemHashScanner, iter);/g' /root/php-src/ext/dom/dom_iterators.c; \
fi; \
else \
Expand Down Expand Up @@ -439,7 +436,7 @@ RUN if [ "$WITH_ICONV" = "yes" ]; \
# PHP <= 7.3 requires Bison 2.7
# PHP >= 7.4 and Bison 3.0
COPY --from=emscripten-bison-2-7 /usr/local/bison /root/linked-bison-27
RUN if [[ "${PHP_VERSION:0:1}" -le "7" && "${PHP_VERSION:2:1}" -le "3" ]] || [ "${PHP_VERSION:0:1}" -le "5" ]; then \
RUN if [[ "${PHP_VERSION:0:1}" -le "7" && "${PHP_VERSION:2:1}" -le "3" ]]; then \
mv /root/linked-bison-27 /usr/local/bison && \
ln -s /usr/local/bison/bin/bison /usr/bin/bison && \
ln -s /usr/local/bison/bin/yacc /usr/bin/yacc; \
Expand Down Expand Up @@ -519,11 +516,14 @@ 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

# 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.
RUN if [[ "${PHP_VERSION:0:1}" -le "7" && "${PHP_VERSION:2:1}" -le "3" ]] || [ "${PHP_VERSION:0:1}" -le "5" ]; then \
RUN if [[ "${PHP_VERSION:0:1}" -le "7" && "${PHP_VERSION:2:1}" -le "3" ]]; then \
echo '#define HAVE_POSIX_READDIR_R 1' >> /root/php-src/main/php_config.h; \
fi;

Expand All @@ -534,15 +534,31 @@ 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

# Provide a custom implementation of the php_exec() function that handles spawning
# the process inside exec(), passthru(), system(), etc.
# We effectively remove the php_exec() implementation from the build by renaming it
# to an unused identifier "php_exec_old", and then we mark php_exec as extern.
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

# Provide a custom implementation of the VCWD_POPEN() function that handles spawning
# the process inside PHP_FUNCTION(popen).
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;

# Don't ship PHP_FUNCTION(proc_open) with the PHP build
# so that we can ship a patched version with php_wasm.c
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 @@ -551,6 +567,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 @@ -574,30 +600,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 @@ -617,9 +646,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 @@ -651,7 +682,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 @@ -707,6 +745,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 @@ -792,6 +832,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 @@ -914,6 +955,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 @@ -934,8 +984,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 @@ -945,6 +994,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 @@ -984,6 +1035,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