[php-wasm] Add support for rm command in sandboxed spawn handler#3010
[php-wasm] Add support for rm command in sandboxed spawn handler#3010Utsav-Ladani wants to merge 6 commits intoWordPress:trunkfrom
rm command in sandboxed spawn handler#3010Conversation
packages/php-wasm/universal/src/lib/sandboxed-spawn-handler-factory.ts
Outdated
Show resolved
Hide resolved
3903795 to
cf0f654
Compare
2ecba1b to
89d198e
Compare
| } | ||
| case 'rm': { | ||
| const target = args[args.length - 1]; | ||
| const flags = args.slice(1, -1).join(' '); |
There was a problem hiding this comment.
It may be more involved than that. Here's a few examples that would be really useful to cover with a unit test to make sure this command can handle them without failing:
rm file1 file2 file3
rm -r -f directory
rm file1 -f file2 -r
# This one means "remove a file called "-rf"
rm -- -rf
# This one means "recursively remove a file or a directory called "-rf"
rm -rf -- -rfThere was a problem hiding this comment.
@adamziel @zaerl What do you think about using the yargs-parser npm package to parse the args instead of writing a custom parser from scratch?
There was a problem hiding this comment.
@Utsav-Ladani potentially! Let's give it a try and see what would this code look like with yargs-parser in place. It seems to be larger than we need, but still fairly small so perhaps? Note this package (@php-wasm/universal) can be imported in both node.js and web browser code, which yargs-parser seems to support. Also, one potentially relevant difference is that yargs-parser seems to accept a string, and in here we already have access to a pre-parsed args array where string syntax were already processed (e.g. "my string" becomes my string) – hopefully there's a way to work from there without re-encoding that and re-escaping quotes.
There was a problem hiding this comment.
@adamziel I have updated the PR to try yargs-parser and it works for all cases you mentioned above, but we have an issue with the import.
import yargsParser from 'yargs-parser/browser'; is throwing process is not defined error as we are running it in the browser and import yargsParser from 'yargs-parser/browser'; is throwing type error.
89d198e to
96129a6
Compare
There was a problem hiding this comment.
Pull request overview
Adds rm support to the sandboxed spawn handler so WP-CLI commands like wp plugin uninstall (which uses rm -rf) can run successfully in the sandbox.
Changes:
- Allow
rmas a supported binary in the sandboxed spawn handler. - Implement an
rmhandler (with-r/-fparsing) to delete files/directories. - Refactor repeated
setTimeoutdelay logic into a sharedwait()helper.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (await php.isDir(target)) { | ||
| if (isRecursive) { | ||
| await php.rmdir(target, { recursive: true }); | ||
| } else { | ||
| errorMessages.push( | ||
| `rm: ${target}: is a directory` | ||
| ); | ||
| } | ||
| } else if (await php.isFile(target)) { | ||
| await php.unlink(target); | ||
| } else if (!isForce) { | ||
| errorMessages.push( | ||
| `rm: ${target}: No such file or directory` | ||
| ); |
There was a problem hiding this comment.
Any exception thrown by php.isDir, php.isFile, php.rmdir, or php.unlink will bubble to the outer catch, which is documented as meaning 'the PHP runtime has crashed'. That will incorrectly treat routine filesystem errors (permission issues, transient FS errors, unexpected path formats) as a runtime crash. Handle expected per-target FS failures inside the rm case (e.g., wrap operations per target in try/catch, emit an rm:-style message to stderr, and set a non-zero exit code) so the handler reports command errors instead of misclassifying them as runtime crashes.
| if (await php.isDir(target)) { | |
| if (isRecursive) { | |
| await php.rmdir(target, { recursive: true }); | |
| } else { | |
| errorMessages.push( | |
| `rm: ${target}: is a directory` | |
| ); | |
| } | |
| } else if (await php.isFile(target)) { | |
| await php.unlink(target); | |
| } else if (!isForce) { | |
| errorMessages.push( | |
| `rm: ${target}: No such file or directory` | |
| ); | |
| try { | |
| if (await php.isDir(target)) { | |
| if (isRecursive) { | |
| await php.rmdir(target, { recursive: true }); | |
| } else { | |
| errorMessages.push( | |
| `rm: ${target}: is a directory` | |
| ); | |
| } | |
| } else if (await php.isFile(target)) { | |
| await php.unlink(target); | |
| } else if (!isForce) { | |
| errorMessages.push( | |
| `rm: ${target}: No such file or directory` | |
| ); | |
| } | |
| } catch (e) { | |
| const message = | |
| e instanceof Error ? e.message : String(e); | |
| errorMessages.push(`rm: ${target}: ${message}`); |
packages/php-wasm/universal/src/lib/sandboxed-spawn-handler-factory.ts
Outdated
Show resolved
Hide resolved
| const parsedArgs = yargsParser(args.slice(1), { | ||
| alias: { | ||
| recursive: ['r'], | ||
| force: ['f'], | ||
| }, | ||
| boolean: ['recursive', 'force'], | ||
| }) as any; |
There was a problem hiding this comment.
Avoid as any here—it's easy to accidentally access missing properties (e.g., parsedArgs._) without type checking and makes future refactors riskier. Prefer a small local type/interface for the subset you rely on (e.g., { _: string[]; recursive?: boolean; force?: boolean }), or use a typed return shape so targets, isRecursive, and isForce are derived from well-defined fields.
Motivation for the change
The
wp plugin uninstall <plugin-name>command usesrm -rfto delete the plugin files, and the current sandboxed spawn handler doesn't have support for it, so I am just adding it.Closes: #2653
Implementation details
Adding support for
rmin the sandboxed spawn handler.Testing Instructions (or ideally a Blueprint)
The following blueprint should run without any error, and the
Hello Dollyplugin should be deleted:{ "login": true, "landingPage": "/wp-admin/plugins.php", "steps": [ { "step": "wp-cli", "command": "wp plugin uninstall hello" } ] }