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_EVENTto 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.