Skip to content

Commit

Permalink
Add npm run fix-asyncify task that adds the missing ASYNCIFY_ONLY i…
Browse files Browse the repository at this point in the history
…tems (WordPress#253)

With this commit, fixing Asyncify issues only requires:

1. Adding a test case that triggers a crash to `packages/php-wasm/node/src/test/php-asyncify.spec.ts`
2. Running:

```bash
npm run fix-asyncify
```

The fix-asyncify command:

1. Runs Asyncify test suite that makes PHP trigger an async call through all known code paths. If it works, we're done. If it fails, keep going.
2. Automatically adds any missing C functions to the ASYNCIFY_ONLY list in the Dockerfile
3. Rebuilds PHP
4. Loops to 1

To make it work, this PR:

* Converts unhandled Asyncify rejections into catchable errors thrown inside `PHP.run()`
* Logs PHP functions at the time of an async call, not at the time of rewinding
* Prevents Node.js from exiting or hiding the error message when the WASM module calls exit()

## Follow-up work

* Add a documentation page

cc @sejas @danielbachhuber
  • Loading branch information
adamziel authored May 9, 2023
1 parent 2b01f8d commit 98a9d8d
Show file tree
Hide file tree
Showing 47 changed files with 913 additions and 136 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"recompile:php:node:8.2": "nx recompile-php php-wasm-node --PHP_VERSION=8.2",
"release": "lerna publish patch --yes --no-private --loglevel=verbose",
"test": "nx run-many --all --target=test",
"fix-asyncify": "node packages/php-wasm/node/bin/rebuild-while-asyncify-functions-missing.mjs",
"typecheck": "nx run-many --all --target=typecheck"
},
"private": true,
Expand Down
45 changes: 40 additions & 5 deletions packages/php-wasm/compile/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -556,11 +556,10 @@ RUN if [ "$WITH_WS_NETWORKING_PROXY" = "yes" ]; \
# 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 ...
Expand Down Expand Up @@ -621,7 +620,40 @@ RUN if [ "$WITH_WS_NETWORKING_PROXY" = "yes" ]; \
"dynCall_viiiii",\
"dynCall_viiiiiii",\
"dynCall_viiiiiiii",'; \
export ASYNCIFY_ONLY=$'"ZEND_DO_FCALL_BY_NAME_SPEC_HANDLER",\
export ASYNCIFY_ONLY=$'"zend_call_known_instance_method_with_2_params",\
"zend_fetch_dimension_address_read_R",\
"_zval_dtor_func_for_ptr",\
"ZEND_ASSIGN_DIM_SPEC_CV_CONST_HANDLER",\
"zend_fetch_dimension_address_read",\
"php_if_fopen",\
"zend_std_has_dimension",\
"zend_isset_isempty_dim_prop_obj_handler_SPEC_CV_CONST",\
"zend_assign_to_object",\
"ZEND_ASSIGN_OBJ_SPEC_CV_CONST_HANDLER",\
"zend_std_call_user_call",\
"zend_objects_store_del_ref_by_handle_ex",\
"zend_objects_store_del_ref",\
"_zval_dtor_func",\
"_zval_ptr_dtor",\
"zend_hash_del_key_or_index",\
"zend_delete_variable",\
"ZEND_UNSET_VAR_SPEC_CV_UNUSED_HANDLER",\
"zend_std_unset_dimension",\
"ZEND_UNSET_DIM_SPEC_CV_CONST_HANDLER",\
"zend_std_read_dimension",\
"zend_fetch_dimension_address_read_R_slow",\
"ZEND_FETCH_DIM_R_SPEC_CV_CONST_HANDLER",\
"zend_call_method",\
"zend_assign_to_object_dim",\
"ZEND_ASSIGN_DIM_SPEC_CV_CONST_OP_DATA_CONST_HANDLER",\
"zend_std_write_dimension",\
"zend_isset_dim_slow",\
"ZEND_ISSET_ISEMPTY_DIM_OBJ_SPEC_CV_CONST_HANDLER",\
"zend_std_write_property",\
"ZEND_ASSIGN_OBJ_SPEC_CV_CONST_OP_DATA_CONST_HANDLER",\
"zend_objects_store_del",\
"ZEND_UNSET_CV_SPEC_CV_UNUSED_HANDLER",\
"ZEND_DO_FCALL_BY_NAME_SPEC_HANDLER",\
"ZEND_DO_FCALL_BY_NAME_SPEC_OBSERVER_HANDLER",\
"ZEND_DO_FCALL_BY_NAME_SPEC_RETVAL_UNUSED_HANDLER",\
"ZEND_DO_FCALL_BY_NAME_SPEC_RETVAL_USED_HANDLER",\
Expand Down Expand Up @@ -786,11 +818,11 @@ RUN if [ "$WITH_WS_NETWORKING_PROXY" = "yes" ]; \
"php_stream_url_wrap_http_ex",\
"php_stream_xport_crypto_enable",\
"php_tcp_sockop_set_option",\
"php_tcp_sockop_set_option",\
"preg_replace_func_impl",\
"readline_shell_run",\
"reflection_method_invoke",\
"run_cli",\
"run_php",\
"user_shutdown_function_call",\
"shutdown_executor",\
"shutdown_destructors",\
Expand Down Expand Up @@ -916,6 +948,7 @@ RUN source /root/emsdk/emsdk_env.sh && \
-s ALLOW_MEMORY_GROWTH=1 \
-s ASSERTIONS=0 \
-s ERROR_ON_UNDEFINED_SYMBOLS=0 \
-s NODEJS_CATCH_EXIT=0 \
-s INVOKE_RUN=0 \
-s EXIT_RUNTIME=1 \
/root/lib/libphp.a \
Expand All @@ -939,6 +972,7 @@ RUN ls /root/output/
# fi

# Postprocess the build php.js module:
COPY ./build-assets/append-before-return.js /root/append-before-return.js
RUN \
# Figure out the target file names and URLs
# The .js and .wasm filenames should reflect the build configuration, e.g.:
Expand Down Expand Up @@ -1010,8 +1044,9 @@ RUN \
else \
echo "import dependencyFilename from './$WASM_FILENAME'; " >> /root/output/php-module.js; \
fi; \
echo " export { dependencyFilename }; export function init(RuntimeName, PHPLoader, EnvVariables) {" >> /root/output/php-module.js && \
echo " export { dependencyFilename }; export function init(RuntimeName, PHPLoader) {" >> /root/output/php-module.js && \
cat /root/output/php.js >> /root/output/php-module.js && \
cat /root/append-before-return.js >> /root/output/php-module.js && \
echo " return PHPLoader; }" >> /root/output/php-module.js && \
\
# Remove the old php.js file
Expand Down
15 changes: 15 additions & 0 deletions packages/php-wasm/compile/build-assets/append-before-return.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Debugging Asyncify errors is tricky because the stack trace is lost when the
* error is thrown. This code saves the stack trace in a global variable
* so that it can be inspected later.
*/
PHPLoader.debug = 'debug' in PHPLoader ? PHPLoader.debug : true;
if (PHPLoader.debug) {
const originalHandleSleep = Asyncify.handleSleep;
Asyncify.handleSleep = function (startAsync) {
if (!ABORT) {
Module["lastAsyncifyStackSource"] = new Error();
}
return originalHandleSleep(startAsync);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { execSync, spawnSync } from 'child_process';
import { createHash } from 'crypto';
import fs from 'fs';
import path from 'path';
import { hash } from 'ts-json-schema-generator';
import { phpVersions } from '../../supported-php-versions.mjs';

const PHP_VERSIONS = process.env.PHP
? [process.env.PHP]
: phpVersions.map(({ version }) => version);
let hash1 = '';
let hash2 = '';
for (const PHP_VERSION of PHP_VERSIONS) {
while (true) {
console.log(`Running asyncify tests for PHP ${PHP_VERSION}...`);
hash1 = getHash();
// Need to reset nx server/cache or otherwise
// all the test runs will come from cache.
spawnSync('node', ['./node_modules/.bin/nx', 'reset']);
spawnSync(
'node',
[
'--stack-trace-limit=100',
'./node_modules/.bin/nx',
'test',
'php-wasm-node',
'--test-name-pattern=asyncify',
],
{
env: {
...process.env,
PHP: PHP_VERSION,
FIX_DOCKERFILE: 'true',
},
}
);

hash2 = getHash();
if (hash1 === hash2) {
console.log(`Dockerfile did not change!`);
break;
}

console.log(`Dockerfile changed, recompiling PHP...`);
spawnSync('npm', ['run', `recompile:php:node:${PHP_VERSION}`]);
}
}

function getHash() {
return createHash('sha1')
.update(
fs.readFileSync(
new URL('../../compile/Dockerfile', import.meta.url).pathname
)
)
.digest('hex');
}
2 changes: 1 addition & 1 deletion packages/php-wasm/node/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
"minify": false
},
"production": {
"minify": true
"minify": false
}
}
},
Expand Down
24 changes: 17 additions & 7 deletions packages/php-wasm/node/public/php_5_6.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export const dependenciesTotalSize = 10157347;
export const dependenciesTotalSize = 10176137;
const dependencyFilename = __dirname + '/php_5_6.wasm';
export { dependencyFilename }; export function init(RuntimeName, PHPLoader, EnvVariables) {
export { dependencyFilename }; export function init(RuntimeName, PHPLoader) {
var Module = typeof PHPLoader != "undefined" ? PHPLoader : {};

var moduleOverrides = Object.assign({}, Module);
Expand Down Expand Up @@ -76,11 +76,6 @@ if (ENVIRONMENT_IS_NODE) {
if (typeof module != "undefined") {
module["exports"] = Module;
}
process["on"]("uncaughtException", function(ex) {
if (!(ex instanceof ExitStatus)) {
throw ex;
}
});
process["on"]("unhandledRejection", function(reason) {
throw reason;
});
Expand Down Expand Up @@ -7034,4 +7029,19 @@ if (Module["preInit"]) {
}

run();
/**
* Debugging Asyncify errors is tricky because the stack trace is lost when the
* error is thrown. This code saves the stack trace in a global variable
* so that it can be inspected later.
*/
PHPLoader.debug = 'debug' in PHPLoader ? PHPLoader.debug : true;
if (PHPLoader.debug) {
const originalHandleSleep = Asyncify.handleSleep;
Asyncify.handleSleep = function (startAsync) {
if (!ABORT) {
Module["lastAsyncifyStackSource"] = new Error();
}
return originalHandleSleep(startAsync);
}
}
return PHPLoader; }
Binary file modified packages/php-wasm/node/public/php_5_6.wasm
Binary file not shown.
24 changes: 17 additions & 7 deletions packages/php-wasm/node/public/php_7_0.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export const dependenciesTotalSize = 10371564;
export const dependenciesTotalSize = 10389382;
const dependencyFilename = __dirname + '/php_7_0.wasm';
export { dependencyFilename }; export function init(RuntimeName, PHPLoader, EnvVariables) {
export { dependencyFilename }; export function init(RuntimeName, PHPLoader) {
var Module = typeof PHPLoader != "undefined" ? PHPLoader : {};

var moduleOverrides = Object.assign({}, Module);
Expand Down Expand Up @@ -76,11 +76,6 @@ if (ENVIRONMENT_IS_NODE) {
if (typeof module != "undefined") {
module["exports"] = Module;
}
process["on"]("uncaughtException", function(ex) {
if (!(ex instanceof ExitStatus)) {
throw ex;
}
});
process["on"]("unhandledRejection", function(reason) {
throw reason;
});
Expand Down Expand Up @@ -7015,4 +7010,19 @@ if (Module["preInit"]) {
}

run();
/**
* Debugging Asyncify errors is tricky because the stack trace is lost when the
* error is thrown. This code saves the stack trace in a global variable
* so that it can be inspected later.
*/
PHPLoader.debug = 'debug' in PHPLoader ? PHPLoader.debug : true;
if (PHPLoader.debug) {
const originalHandleSleep = Asyncify.handleSleep;
Asyncify.handleSleep = function (startAsync) {
if (!ABORT) {
Module["lastAsyncifyStackSource"] = new Error();
}
return originalHandleSleep(startAsync);
}
}
return PHPLoader; }
Binary file modified packages/php-wasm/node/public/php_7_0.wasm
Binary file not shown.
24 changes: 17 additions & 7 deletions packages/php-wasm/node/public/php_7_1.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export const dependenciesTotalSize = 10567085;
export const dependenciesTotalSize = 10583644;
const dependencyFilename = __dirname + '/php_7_1.wasm';
export { dependencyFilename }; export function init(RuntimeName, PHPLoader, EnvVariables) {
export { dependencyFilename }; export function init(RuntimeName, PHPLoader) {
var Module = typeof PHPLoader != "undefined" ? PHPLoader : {};

var moduleOverrides = Object.assign({}, Module);
Expand Down Expand Up @@ -76,11 +76,6 @@ if (ENVIRONMENT_IS_NODE) {
if (typeof module != "undefined") {
module["exports"] = Module;
}
process["on"]("uncaughtException", function(ex) {
if (!(ex instanceof ExitStatus)) {
throw ex;
}
});
process["on"]("unhandledRejection", function(reason) {
throw reason;
});
Expand Down Expand Up @@ -7000,4 +6995,19 @@ if (Module["preInit"]) {
}

run();
/**
* Debugging Asyncify errors is tricky because the stack trace is lost when the
* error is thrown. This code saves the stack trace in a global variable
* so that it can be inspected later.
*/
PHPLoader.debug = 'debug' in PHPLoader ? PHPLoader.debug : true;
if (PHPLoader.debug) {
const originalHandleSleep = Asyncify.handleSleep;
Asyncify.handleSleep = function (startAsync) {
if (!ABORT) {
Module["lastAsyncifyStackSource"] = new Error();
}
return originalHandleSleep(startAsync);
}
}
return PHPLoader; }
Binary file modified packages/php-wasm/node/public/php_7_1.wasm
Binary file not shown.
24 changes: 17 additions & 7 deletions packages/php-wasm/node/public/php_7_2.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export const dependenciesTotalSize = 10977877;
export const dependenciesTotalSize = 10993263;
const dependencyFilename = __dirname + '/php_7_2.wasm';
export { dependencyFilename }; export function init(RuntimeName, PHPLoader, EnvVariables) {
export { dependencyFilename }; export function init(RuntimeName, PHPLoader) {
var Module = typeof PHPLoader != "undefined" ? PHPLoader : {};

var moduleOverrides = Object.assign({}, Module);
Expand Down Expand Up @@ -76,11 +76,6 @@ if (ENVIRONMENT_IS_NODE) {
if (typeof module != "undefined") {
module["exports"] = Module;
}
process["on"]("uncaughtException", function(ex) {
if (!(ex instanceof ExitStatus)) {
throw ex;
}
});
process["on"]("unhandledRejection", function(reason) {
throw reason;
});
Expand Down Expand Up @@ -7016,4 +7011,19 @@ if (Module["preInit"]) {
}

run();
/**
* Debugging Asyncify errors is tricky because the stack trace is lost when the
* error is thrown. This code saves the stack trace in a global variable
* so that it can be inspected later.
*/
PHPLoader.debug = 'debug' in PHPLoader ? PHPLoader.debug : true;
if (PHPLoader.debug) {
const originalHandleSleep = Asyncify.handleSleep;
Asyncify.handleSleep = function (startAsync) {
if (!ABORT) {
Module["lastAsyncifyStackSource"] = new Error();
}
return originalHandleSleep(startAsync);
}
}
return PHPLoader; }
Binary file modified packages/php-wasm/node/public/php_7_2.wasm
Binary file not shown.
24 changes: 17 additions & 7 deletions packages/php-wasm/node/public/php_7_3.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export const dependenciesTotalSize = 10905567;
export const dependenciesTotalSize = 10915927;
const dependencyFilename = __dirname + '/php_7_3.wasm';
export { dependencyFilename }; export function init(RuntimeName, PHPLoader, EnvVariables) {
export { dependencyFilename }; export function init(RuntimeName, PHPLoader) {
var Module = typeof PHPLoader != "undefined" ? PHPLoader : {};

var moduleOverrides = Object.assign({}, Module);
Expand Down Expand Up @@ -76,11 +76,6 @@ if (ENVIRONMENT_IS_NODE) {
if (typeof module != "undefined") {
module["exports"] = Module;
}
process["on"]("uncaughtException", function(ex) {
if (!(ex instanceof ExitStatus)) {
throw ex;
}
});
process["on"]("unhandledRejection", function(reason) {
throw reason;
});
Expand Down Expand Up @@ -7153,4 +7148,19 @@ if (Module["preInit"]) {
}

run();
/**
* Debugging Asyncify errors is tricky because the stack trace is lost when the
* error is thrown. This code saves the stack trace in a global variable
* so that it can be inspected later.
*/
PHPLoader.debug = 'debug' in PHPLoader ? PHPLoader.debug : true;
if (PHPLoader.debug) {
const originalHandleSleep = Asyncify.handleSleep;
Asyncify.handleSleep = function (startAsync) {
if (!ABORT) {
Module["lastAsyncifyStackSource"] = new Error();
}
return originalHandleSleep(startAsync);
}
}
return PHPLoader; }
Binary file modified packages/php-wasm/node/public/php_7_3.wasm
Binary file not shown.
Loading

0 comments on commit 98a9d8d

Please sign in to comment.