Map: repl / jobs

ral/src/jobs.rs is interactive job control (SPEC §11.6). 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 iteration and on exit by the session. fg/bg/disown name their job explicitly: each declares exactly one Int id, so the checker guarantees the argument and job_id_arg has nothing to default to. Declaring their arguments makes all three table entries and so natives, and $fg is a value, host-captured and first-class at once, as the nullary jobs already is (fixed-arity). 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.

A job also owns whatever atomic writes its members staged but have not finished. Escape::Stopped carries them out of the evaluator as PendingWrites — a tmp path and a target path, no open file — and JobTable::settle decides each one when the job ends: renamed onto the target if the group leader exited 0, unlinked on every other ending, disown and the exit sweep included. settle is the only way a row leaves the table, so a job cannot be dropped with its writes left stranded. This is why wait_foreground and reap keep the leader’s exit status instead of draining past it.

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 — spawn 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 run is given, not a predicate wait_foreground re-derives: it arrives as the &Mooring that threads beside the shell. Shell::terminal_lease yields Some(&TerminalLease) only when that mooring’s terminal 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.

Two populations, one listing

spawn { sleep 10 } yields an in-process handle, invisible to jobs until this fold. 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.