[PHP-WASM] Add per-command spawn handlers - #3995
Conversation
Route every process spawn through a PHP-level dispatcher so specific binaries (by argv[0] basename) can be intercepted via setCommandSpawnHandler() regardless of the generic setSpawnHandler(). Spawn handlers now live on the PHP instance rather than the Emscripten runtime, so they survive hotSwapPHPRuntime() without being copied over. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds a per-command spawn-handler dispatch layer to PHP so specific binaries (by basename(argv[0]), e.g. sendmail) can be intercepted without being overridden by a later generic setSpawnHandler() call, and so handlers survive hotSwapPHPRuntime().
Changes:
- Introduces
PHP.setCommandSpawnHandler(command, handler)and a private#dispatchSpawn()router. - Moves spawn handler state onto the
PHPinstance and installs a runtime-level dispatcher ininitializeRuntime(). - Updates hot-swap logic to stop copying
runtime.spawnProcessbetween runtimes.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| packages/php-wasm/universal/src/lib/php.ts | Adds per-command spawn handlers and a centralized dispatcher; updates runtime initialization and hot-swap behavior accordingly. |
| packages/php-wasm/universal/src/lib/php.spec.ts | Adjusts a runtime mock shape in tests (removes spawnProcess field). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
@adamziel I would love your feedback on this PR as the rest of SMTP depends on it. |
| ? [command as string, ...args] | ||
| : splitShellCommand(command as string); | ||
| const commandSpawnHandler = | ||
| commandArray[0] && |
There was a problem hiding this comment.
Not a blocker: This won't handle ENV=value php script.php. The current spawn handles doesn't either so we do not regress, so it's fine.
| // The runtime never sees user-provided spawn handlers directly – | ||
| // every spawn flows through the dispatcher so that per-command | ||
| // handlers cannot be displaced by a setSpawnHandler() call. | ||
| runtime.spawnProcess = ( |
There was a problem hiding this comment.
This seems to break passing spawnProcess to the PHP initialization handler, so this wouldn't work anymore:
import { spawn } from 'node:child_process';
import { loadNodeRuntime } from '@php-wasm/node';
import { PHP } from '@php-wasm/universal';
const php = new PHP(
await loadNodeRuntime('8.4', {
emscriptenOptions: {
spawnProcess(command: string, args: string[], options: any) {
return spawn(command, args, {
...options,
shell: true,
});
},
},
})
);
const result = await php.run({
code: `<?php echo exec('echo Playground');`,
});
console.log(result.text); // PlaygroundIs anyone relying on this semantics? If yes, what would we break and how could we preserve this semantics? If noone is using it, I think we're fine.
There was a problem hiding this comment.
I've done some research on SourceGraph and GitHub:
- Global spawnProcess emscriptenOptions: 0 matches.
- Global spawnProcess "@php-wasm": 0 matches.
I think we're good.
There was a problem hiding this comment.
Thank you for figuring this out, I completely missed that we also allow to set the spawnHandler using emscriptenOptions.spawnProcess
…hp-command-spawn-handlers # Conflicts: # packages/php-wasm/universal/src/lib/php.spec.ts
PHP only had a single spawn hook. It got the command and had to decide what process to start. That is too coarse for integrations that need to own one binary, such as
sendmail, while leaving the rest of process spawning to the normal handler.This adds
PHP.setCommandSpawnHandler(command, handler). The command is matched against the basename ofargv[0], so/usr/sbin/sendmailhits asendmailhandler. A command-specific handler wins over the genericsetSpawnHandler()callback. Everything else still goes to the generic spawn handler.The matcher is deliberately not a shell.
ENV=value sendmaildoes not hit thesendmailhandler, becauseENV=valueis the first token. That is not a regression: there was no command-specific dispatch before, and callers that need shell syntax can still use the generic spawn handler.The dispatch point now lives on the
PHPinstance instead of storing user handlers directly on the Emscripten runtime. A runtime hot swap gets a fresh dispatcher, and the registered handlers stay attached to the PHP object. If no handler matches, the dispatcher throws the sameSPAWN_UNSUPPORTEDerror as before, so PHP still seesENOSYSforpopen()andproc_open().The unit test covers the unsupported-spawn path, command-specific routing, and fallback to the generic handler.