Manual PTY job-control check
Canonical Quirl project documentation synced from crates/quirl-process/MANUAL_JOB_CONTROL.md.
Failure model and invariants
The native executor treats pipeline construction as a transaction and terminal ownership as a lease. These are the failure cases that every implementation and real-PTY check must preserve:
- Partial spawn: every child and descriptor is owned immediately. Failure after any spawn kills the process group, kills each direct child as a fallback, reaps every direct child, closes pending pipe ends, and returns the original construction error.
- Leader exit before group formation: the parent verifies each child's process-group membership after spawn. It never continues with an assumed group, and cleanup addresses both the group and each direct child so a fast leader cannot strand later stages.
- Terminal handoff failure: children remain inside the construction guard until handoff succeeds. A failed handoff terminates and reaps them. Once handoff succeeds, an RAII lease restores Quirl's foreground group and saved termios on success, stop, cancellation, wait failure, and unwinding.
- Stopped child: observing one stopped pipeline member stops every live
member before the job is committed. A stopped job retains its children and
bounded drain tasks until
fg,bg, cancellation, completion, or executor destruction owns the next transition. - Cancellation and deadline: each bounded polling turn observes the request state. Cancellation or expiry terminates the whole contained process tree, reaps direct children, drains or closes capture streams, and returns the cancellation error rather than a cleanup error.
- Drains: capture readers continuously drain child output while retaining only the configured byte budget. Here-string writers remain owned by the job while stopped and are joined after completion or termination.
- Cleanup failure: cleanup is best-effort across every owned resource; one failed group operation does not skip direct-child cleanup. When an operating error already exists, cleanup cannot replace it. A standalone explicit lifecycle operation reports its own cleanup failure with actionable context.
- Retained jobs: a
NativeExecutorretains at most 1,024 job records. At capacity it refreshes state and removes completed records before accepting a new job; 1,024 still-live records fail early withResourceLimit. - Job IDs: zero is never issued. Allocation wraps from
u32::MAXto one and scans the bounded retained table, so an ID cannot collide with a visible job. - Redirects: redirect targets are opened in source order and the last input redirect supplies standard input. Earlier opens still take effect or fail, matching shell descriptor-order semantics.
- Reference dialects: Bash and Zsh compatibility runners are explicitly noninteractive. Their standard input is closed; they remain process-group or Job-Object contained, continuously drain bounded captures, and observe cancellation. Scripts that need interactive reads must be run directly in a terminal outside this compatibility boundary.
Every wait above is either request-bounded or advances in bounded polling turns. Foreground interactive commands may intentionally run until they exit, stop, or receive a terminal signal; they do not retain output in memory.
The canonical Unix PTY harness automates foreground-group ownership, native
Ctrl-Z/Ctrl-C, jobs/bg/fg, fast-leader and construction-failure
races, stopped-job termios preservation, prompt restoration, and the explicit
noninteractive dialect-island policy:
cargo build -p quirl-cli
cargo xtask rich-ptyThe following remains a useful release smoke check in a maintainer's terminal after the automated harness passes.
cargo run -p quirl-cliAt the Quirl prompt:
- Run
sleep 30, pressCtrl-Z, then runjobs. The job must bestoppedand the prompt must accept input normally. - Run
bg %1, thenjobs. The same job id must berunning. - Run
fg %1, then pressCtrl-C. Quirl must regain the terminal and the next prompt must be usable without an extra keypress. - Run
sh -c 'printf out; printf err >&2' | cat. Both streams must complete; Quirl must not hang. - Run
printf hidden > /tmp/quirl-process-manual | cat, thencat /tmp/quirl-process-manual. The pipeline prints nothing and the file containshidden.
Repeat steps 1–3 on both Linux and macOS before calling the Preview job-control gate complete.