Skip to content

Forward host stdin to wp-cli through the server daemon#3211

Open
chubes4 wants to merge 1 commit intotrunkfrom
wp-cli-stdin-forwarding-clean
Open

Forward host stdin to wp-cli through the server daemon#3211
chubes4 wants to merge 1 commit intotrunkfrom
wp-cli-stdin-forwarding-clean

Conversation

@chubes4
Copy link
Copy Markdown
Contributor

@chubes4 chubes4 commented Apr 23, 2026

What

studio wp <command> now forwards piped host stdin to PHP. Closes #3200.

# Before: silently empty
echo 'hello' | studio wp eval 'echo file_get_contents("php://stdin");'
# ""

# After
echo 'hello' | studio wp eval 'echo file_get_contents("php://stdin");'
# hello

# Real-world usage
cat content.md | studio wp post create - --post_title="Draft"
curl -sL plugin.zip | studio wp plugin install - --activate

Why

studio wp runs in a different process from the WordPress server daemon (or in a worker thread on the in-proc path). Neither has the user's stdin pipe attached, so anything piped on the host never reached PHP. php://stdin always read empty.

How

  1. Drain host stdin up-front in commands/wp.tsdrainHostStdin() reads process.stdin into a Buffer when non-TTY (interactive shells are never blocked). Forwarded to both the daemon path (sendWpCliCommand) and the in-proc path (runWpCliCommand / runGlobalWpCliCommand).

  2. Base64 over IPC — Node's child_process IPC serializes payloads as JSON, so the wp-cli-command IPC message gets an optional stdinBase64 field to preserve binary bytes (gzipped SQL dumps, ZIP files, etc.) untouched.

  3. Daemon decodes + forwards — the daemon child in wordpress-server-child.ts decodes base64 and hands the bytes to server.playground.cli(args, { stdin }).

  4. Disable dedup when stdin is presentsequential() previously deduped concurrent WP-CLI calls by args alone. Two callers piping different bytes into the same command would coincidentally collapse into one execution, with the second caller getting the first caller's result silently. Fixed by returning undefined from deduplicateKey whenever stdin is present. Piped stdin is inherently non-idempotent; dedup is meaningless for it.

Requires

This PR relies on the new stdin option on PHP.cli() added in WordPress/wordpress-playground#3523. Merge order: Playground first → release → Studio bumps the version pin → this PR merges.

Tests

Validated end-to-end against a real Studio site:

  • 100 sequential calls with varying stdin → 100/100
  • 20 parallel with identical length, different content → 20/20 (regression test for the dedup fix)
  • 5× 1 MB parallel with md5 roundtrip → 5/5 byte-perfect
  • 30 alternating stdin / no-stdin → 30/30
  • 10 MB random binary md5 roundtrip → byte-perfect
  • UTF-8, binary with nulls / high bytes, empty stdin, no-trailing-newline, shell-special chars — all preserved
  • Real wp post create - --post_title=... creates posts with piped markdown content
  • Daemon path + in-proc fallback (site stopped) both work
  • No behavior change for commands invoked without piped stdin

No behavior change when stdin is a TTY

drainHostStdin() returns undefined when process.stdin.isTTY, so interactive invocations are unaffected. Existing args-based dedup still applies to no-stdin calls (only disabled when bytes are present).

Files

  • apps/cli/commands/wp.tsdrainHostStdin(), forward to both paths
  • apps/cli/lib/run-wp-cli-command.ts — in-proc paths accept { stdin }, forward to php.cli()
  • apps/cli/lib/types/wordpress-server-ipc.ts — add optional stdinBase64 to IPC schema
  • apps/cli/lib/wordpress-server-manager.tssendWpCliCommand accepts Buffer, base64-encodes it
  • apps/cli/wordpress-server-child.ts — decode base64, forward to server.playground.cli({ stdin }), fix dedup key

AI assistance

  • AI assistance: Yes
  • Tool(s): Claude Code (Opus 4.7)
  • Used for: Initial implementation of the five-file stdin-forwarding stack (host-stdin drain, IPC stdinBase64 schema, daemon decode, runWpCliCommand passthrough), and the sequential() dedup-key fix. The dedup-collision bug was surfaced by a parallel stress test during review and fixed in the same branch. All code was read, understood, and exercised end-to-end (100 sequential varied stdin, 20 parallel same-length-different-content, 5×1MB md5 roundtrip, alternating stdin/no-stdin, binary/UTF-8/empty/huge payloads) before submission.

`echo foo | studio wp eval 'echo file_get_contents("php://stdin");'`
silently dropped input when the site was running (daemon path). Both
paths now drain process.stdin up-front and forward the bytes to
`php.cli({ stdin })`, which — paired with the Playground-side stdin
option — surfaces them to PHP via `php://stdin`.

- commands/wp.ts: `drainHostStdin()` reads a non-TTY process.stdin
  into a Buffer and passes it to both the daemon (`sendWpCliCommand`)
  and in-proc (`runWpCliCommand`/`runGlobalWpCliCommand`) paths.
- lib/types/wordpress-server-ipc.ts: wp-cli-command IPC payload gains
  an optional `stdinBase64` field. Base64 is used because Node's
  child_process IPC is JSON-serialized; we must preserve binary bytes
  (e.g. gzipped SQL dumps) byte-for-byte.
- lib/wordpress-server-manager.ts: `sendWpCliCommand` accepts an
  optional Buffer and base64-encodes it into the payload.
- lib/run-wp-cli-command.ts: in-proc `runWpCliCommand` /
  `runGlobalWpCliCommand` accept an optional `{ stdin }` and
  forward it to `php.cli()`.
- wordpress-server-child.ts: decode the base64 on the daemon side and
  hand the bytes to `server.playground.cli(args, { stdin })`. Also
  disable the sequential() dedup key whenever stdin is present —
  piped stdin is inherently non-idempotent and two callers with
  coincidentally equal byte lengths must not collapse into one
  execution. The previous key included byte length only, which was
  not enough.

No behavior change for commands invoked without piped stdin.
@wpmobilebot
Copy link
Copy Markdown
Collaborator

📊 Performance Test Results

Comparing 445d9a1 vs trunk

app-size

Metric trunk 445d9a1 Diff Change
App Size (Mac) 1482.50 MB 1482.50 MB +0.00 MB ⚪ 0.0%

site-editor

Metric trunk 445d9a1 Diff Change
load 1771 ms 1538 ms 233 ms 🟢 -13.2%

site-startup

Metric trunk 445d9a1 Diff Change
siteCreation 8095 ms 8087 ms 8 ms ⚪ 0.0%
siteStartup 4944 ms 4946 ms +2 ms ⚪ 0.0%

Results are median values from multiple test runs.

Legend: 🟢 Improvement (faster) | 🔴 Regression (slower) | ⚪ No change (<50ms diff)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

studio wp silently drops host stdin when the site is running (breaks wp db query < file.sql, wp post create --content=-, etc.)

2 participants