Map: repl / jobs

ral/src/jobs.rs is interactive job control (SPEC §18). JobTable tracks background and stopped pipelines keyed by process-group id. Every pipeline stage is placed in its own pgid — set in each stage’s pre_exec on Unix, via CREATE_NEW_PROCESS_GROUP plus a Job Object on Windows — so signalling, waiting, and foreground handoff always target the whole group. There is no pid/pgid ambiguity here: only the group.

Core’s typed waitpgid_eintr / try_waitpgid_eintr funnels keep EINTR opaque, so an interrupted wait is never mistaken for ECHILD (which would flip a live job to “gone”). Blocking and polling results have different types, and a job stores a positive Pgid, not an integer to revalidate at each syscall. Statuses remain total kernel data through rustix’s transparent WaitStatus; stopped, continued, exited, and signalled observations need no fallible enum decode (total-wait-status). The table backs the four captured builtins (host handlers) — jobs, fg, bg, disown — and is reaped each turn and on exit by the session. A bare fg/bg/disown defaults to most_recent_id (the highest, == newest, job id) per SPEC §18. reap requests continued as well as stopped statuses, so a group resumed out-of-band by an external kill -CONT flips back to running (mark_running) rather than reading stopped forever. On exit a job group is taken down in three steps:

  • gracefully — SIGTERM then SIGCONT on Unix (a frozen group must run to act on the request); CTRL_BREAK_EVENT to every group on Windows;
  • given a five-second grace during which natural exits are reaped;
  • then forced — SIGKILL / TerminateJobObject.

Escape::Stopped is Unix-only: a foreground job stopped by SIGTSTP escapes the evaluator and exec.rs records it as a Stopped job. That arm is the table’s only live populator — & registers workers (below) on every platform — so on Windows, which has no SIGTSTP analogue, the table is empty in a live session. It still compiles and operates cfg-free: fg blocks on the leader’s process handle (no console handoff, stopped_by always None), bg has no SIGCONT to send, and a reaped-away group’s stragglers die through the Job Object’s KILL_ON_JOB_CLOSE flag.

On Unix, fg resumes a parked group only through a held terminal lease: the controlling-terminal handoff is an unforgeable TerminalLease the turn is given, not a predicate wait_foreground re-derives. The accessor yields Some(&TerminalLease) only when the installed turn’s access permits and the session owns the lease (see terminal-lease); the ForegroundGuard acquires tcsetpgrp + the termios snapshot only on that borrow. Without a lease — a non-interactive resume — there is no tty dance to do, so fg still SIGCONTs the whole -pgid and waits but skips the handoff.

The wart heals at the listing layer

sleep 10 & desugars to spawn — an in-process handle, invisible to jobs until now. host_handlers.rs::render_jobs folds both populations into one listing: JobTable’s pgid groups exactly as above, then the shell.workers() snapshot it is handed, marked [wN] — a designator namespace of its own so it can never collide with a pgid’s [n]. A worker reads running (worker) while live and done (worker) once settled but unclaimed (the POSIX-Done analogue); once an eliminator observes it away it is simply absent from the next shell.workers() snapshot, so the fold keeps no retention state of its own. fg/bg/disown stay strictly pgid-typed — a numeric id that resolves no pgid job answers with the correspondence (await is a handle’s fg, cancel its kill) rather than a bare “no such job”. Both Job (here) and core’s WorkerEntry implement the small Resident signature (residency, core/src/types/resident.rs); render_jobs reads designator/state_label off it for both populations rather than hand-formatting per chapter, while pgid/cmd stay direct field reads — the honest variance the signature leaves unflattened.

host_handlers.rs::survivor_warning is the other half of the same fold, the deferred survivor warning finally landing as a registry consumer (unify-turn-evaluation): at session Drop, before JobTable::cleanup sweeps undisowned pgid groups exactly as before, it composes one compact line naming every worker handle still running — they die with the process, with no pgid to sweep, so this is their only farewell. None when nothing is running; it never gates or delays the exit it announces.