Skip to content

Commit

Permalink
[PHP] Log a developer-friendly error message whenever an "unreachable…
Browse files Browse the repository at this point in the history
…" error is triggered
  • Loading branch information
adamziel committed May 4, 2023
1 parent a7ec44a commit 15a6609
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 1 deletion.
5 changes: 4 additions & 1 deletion packages/php-wasm/universal/src/lib/base-php.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
PHPRunOptions,
RmDirOptions,
} from './universal-php';
import { improveWASMErrorReporting } from './wasm-error-reporting';

const STRING = 'string';
const NUMBER = 'number';
Expand Down Expand Up @@ -86,6 +87,8 @@ export abstract class BasePHP implements IsomorphicLocalPHP {
throw new Error('Invalid PHP runtime id.');
}
this[__private__dont__use] = runtime;

improveWASMErrorReporting(runtime);
}

/** @inheritDoc */
Expand Down Expand Up @@ -362,7 +365,7 @@ export abstract class BasePHP implements IsomorphicLocalPHP {
* This is awkward, but Asyncify makes wasm_sapi_handle_request return
* Promise<Promise<number>>.
*
* @TODO: Determine if this is a bug in emscripten.
* @TODO: Determine whether this is a bug in emscripten or in our code.
*/
const exitCode = await await this[__private__dont__use].ccall(
'wasm_sapi_handle_request',
Expand Down
94 changes: 94 additions & 0 deletions packages/php-wasm/universal/src/lib/wasm-error-reporting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
const UNREACHABLE_ERROR = `
"unreachable" WASM instruction executed. The typical reason is not listing all
the PHP functions that can yield to JavaScript event loop in the ASYNCIFY_ONLY
during the PHP compilation process. How to proceed? Find the "ASYNCIFY" section in
Dockerfile in the WordPress Playground repository.
Below is a list of all the PHP functions found in the stack trace.
If they're all listed in the Dockerfile, you'll need to trigger this error
again with long stack traces enabled. In node.js, you can do it using
the --stack-trace-limit=100 CLI option: \n\n`;

type Runtime = {
asm: Record<string, unknown>;
};

/**
* Wraps WASM function calls with try/catch that
* provides better error reporting.
*
* @param runtime
*/
export function improveWASMErrorReporting(runtime: Runtime) {
let logged = false;
for (const key in runtime.asm) {
if (typeof runtime.asm[key] == 'function') {
const original = runtime.asm[key] as any;
runtime.asm[key] = function (...args: any[]) {
try {
return original(...args);
} catch (e) {
if (logged || !(e instanceof Object)) {
throw e;
}
logged = true;
let betterMessage = UNREACHABLE_ERROR;
if ('message' in e && e.message === 'unreachable') {
for (const fn of extractPHPFunctionsFromTrace(e)) {
betterMessage += ` * ${fn}\n`;
}
}
errorBox(betterMessage);
throw e;
}
};
}
}
}
// ANSI escape codes for CLI colors and formats
const redBg = '\x1b[41m';
const bold = '\x1b[1m';
const reset = '\x1b[0m';
const eol = '\x1B[K';

function errorBox(message: string) {
console.log(`${redBg}\n${eol}\n${bold} WASM ERROR${reset}${redBg}`);
for (const line of message.split('\n')) {
console.log(`${eol} ${line} `);
}
console.log(`${reset}`);
}

function extractPHPFunctionsFromTrace(e: any) {
if (!e || !('stack' in e)) {
return [];
}
try {
return (e.stack as string)
.split('\n')
.slice(1)
.map((line) => {
const [fn, source] = line
.trim()
.substring('at '.length)
.split(' ');
const filename = source.split(':')[0].split('/').pop() || '';
return {
fn,
isJs:
filename.endsWith('.js') ||
filename.endsWith('.cjs') ||
filename.endsWith('.mjs'),
};
})
.filter(
({ fn, isJs }) =>
!isJs &&
!fn.startsWith('dynCall_') &&
!fn.startsWith('dynCall_ii')
)
.map(({ fn }) => fn);
} catch (err) {
return [];
}
}

0 comments on commit 15a6609

Please sign in to comment.