Repository navigation
feat(process-daemon): run commands without a PTY on Unix - #60697
Conversation
31777cf to
10dd51a
Compare
There was a problem hiding this comment.
I found no blocking problems. On Linux the lib and ownership tests all pass, including the handoff, signal-mask and feature-list tests, and 8 of 10 in tests/pipe.rs pass. cargo fmt --check and clippy -D warnings are clean.
The 2 pipe failures come from my container: its PID 1 never reaps orphans, so the killed sleep processes stay zombies and kill(pid, 0) still says they're alive. terminate did kill them. I left a small inline note on this, since it will also bite anyone running the suite where PID 1 doesn't reap (for example Docker without --init).
Public API: unchanged. The daemon protocol between process-daemon and packages/core only gains optional fields, and the changes are backwards compatible both ways:
create.mode: new, defaults toptytitle,colsandrows: now optionallist.mode: new, and left out of the JSON when unsetTerminalInfo.mode: newExited.signal: new, and left out when absent
Today's TypeScript client ignores the extra mode field, and since list without mode returns only PTYs, it never sees pipe processes.
| } | ||
| } | ||
|
|
||
| fn alive(pid: i32) -> bool { |
There was a problem hiding this comment.
kill(pid, 0) succeeds for zombies too. The sleep/perl grandchildren get reparented to PID 1, so wherever PID 1 doesn't reap (containers without --init, like the one I ran in), wait_dead times out even though terminate killed them. Here, terminate_kills_the_process_group and a_descendant_holding_the_pipe_delays_the_exit_by_at_most_the_drain both failed this way, with ps showing the PIDs in state Z. Treating a zombie as dead would make these tests independent of the host's init, e.g. by checking the state in /proc/<pid>/stat on Linux or ps -o stat=, or by making the test process a subreaper with PR_SET_CHILD_SUBREAPER.
d477d4c to
bf14d89
Compare
There was a problem hiding this comment.
This push only rebases the branch onto dev. The daemon files are byte-for-byte the same as in the commit I reviewed, so my earlier review still holds. The zombie check in tests/pipe.rs is still open.
Public API: unchanged. The internal daemon protocol only gains optional fields and stays backwards compatible.
bf14d89 to
8dfdc15
Compare
There was a problem hiding this comment.
This push only takes --test pipe out of the macOS CI job; the description now says #61277 adds it there instead. Linux CI still runs tests/pipe.rs through the full cargo test --locked. No daemon code or test changed, so my earlier review still holds, and the zombie check in tests/pipe.rs is still open.
Public API: unchanged.
Issue for this PR
Closes #
Type of change
What does this PR do?
Lets the PTY daemon run any command without a terminal ("pipe" processes), so OpenCode can later run its shell commands in the daemon and keep them alive across a server restart handoff. Unix only; Windows comes in a follow-up.
Protocol (additive, stays 7, feature
"pipe"on Unix):createtakesmode: "pty" | "pipe"(default"pty");title,colsandrowsbecome optional and are checked for PTYs only. Older daemons ignoremodeand would start a PTY, so clients send"pipe"only whenfeatureslists it.listtakes an optionalmode. Without it,listreturns PTYs only, so today's servers never see pipe processes as terminal tabs.modeis left out of the JSON when unset, because older daemons reject alistwith extra keys.TerminalInfo.mode;Exited(lifecycle and subscription response) getssignal(for example"SIGTERM"), left out when absent so PTY exits serialize as before.write,resize,control, input frames,snapshot,read_rowsandrenamefail for pipe processes;replay,subscribeandterminatework.How a pipe process runs (
src/service/pipe.rs):std::process::Commandwith its own process group, stdin on/dev/null, and stdout and stderr sharing one pipe, so their bytes stay in order. NoTERM/COLORTERM, no emulator.pre_exechook clears the signal mask and resets SIGCHLD, SIGHUP, SIGINT, SIGQUIT, SIGTERM and SIGALRM to their defaults (like portable-pty does for terminals). The daemon blocks SIGTERM and SIGINT (since fix(process-daemon): stop cleanly on SIGTERM and SIGINT #60691), and std keeps the mask across exec, so without this a pipe child could never be stopped by those signals.waitid(WNOWAIT)and leaves the child as a zombie untilterminate. That keeps its PID and process group ID reserved, soterminatecan always kill leftover descendants in the group (and the child itself, in case it moved to another group) without ever signalling a reused PID.cmd &can't hold the exit forever), and one closes at shutdown (so a descendant that left the group can't blockterminate). Once every writer has closed the output pipe, the rest is read to the end regardless of the 1 s deadline, so a slow subscriber can't truncate the output.pipe2and a fork betweenpipe()and close-on-exec would leak a pipe into another child.In
service.rs: the actor's ghostty terminal is now optional (none for pipe processes); actor startup moved intostart_actorso both spawn paths share it;list_modefilters before refreshing foreground processes.How did you verify your code works?
cargo fmt --check,cargo clippy --all-targets --all-features -- -D warningsand the macOS test suite inpackages/process-daemon; clippy forx86_64-pc-windows-msvcandx86_64-unknown-linux-gnu. Newtests/pipe.rs(Linux CI runs it; #61277 adds it to the macOS CI job):SIGPIPEis not inherited as ignored (a shell that sends itselfSIGPIPEdies from it)SIGTERMsleep 30 &delays the exit by at most the drain, andterminatethen kills itsetsidand holds the pipe doesn't blockterminateterminate, which reaps itDaemon-level tests (real binary): a pipe child started by the daemon still dies from SIGTERM and SIGINT (this test fails without the
pre_execreset); a pipe process keeps running across a restart handoff and delivers its full output and exit code to the new owner; defaultliststays PTY-only;featuresincludespipe. Protocol unit tests pin the wire format for older peers ({"op":"list"}, PTYcreateunchanged,Exitedwithoutsignal).Checklist
Added by the opencode bot (AI-generated).
Related: #36348, #57977