@@ -89,7 +89,7 @@ type Runner struct {
8989 // it is applied by the kernel rather than by [DefaultOpenHandler]. They
9090 // are the same value in bash, where the shell's umask is the process's;
9191 // here they are separate, and this one is what a custom open handler —
92- // backed by an in-memory filesystem, say — has to honour itself.
92+ // backed by an in-memory filesystem, say — has to honor itself.
9393 umask uint32
9494
9595 // Params are the current shell parameters, e.g. from running a shell
@@ -207,8 +207,8 @@ type Runner struct {
207207 lastExpandExit exitStatus // used to surface exit statuses while expanding fields
208208
209209 // bgProcs holds all background shells spawned by this runner.
210- // Their PIDs are fake — a counter with a "g" prefix, to distinguish them
211- // from real PIDs on the host operating system ; see [bgProc.id].
210+ // As they run as goroutines rather than forked processes, they have
211+ // fake PIDs; see [bgProc.id].
212212 //
213213 // Note that each shell only tracks its direct children;
214214 // subshells do not share nor inherit the background PIDs they can wait for.
@@ -227,13 +227,30 @@ type Runner struct {
227227 // bgProcs, so that reaping cannot hand out an id twice.
228228 bgProcSeq int
229229
230+ // jobsBase is the context every background job started by this shell
231+ // descends from. A job must not die because the statement which started
232+ // it is done, and in particular a job nested inside another job outlives
233+ // the one that started it, as in bash. A non-interactive shell's Run call
234+ // bounds it, so an embedder which cancels that still reaps every job, and
235+ // an interactive one keeps its jobs across lines.
236+ jobsBase context.Context
237+
238+ // inSubshell marks a runner made by [Runner.subshell], which takes
239+ // jobsBase from its parent rather than from the Run it is given.
240+ inSubshell bool
241+
230242 // bgStarted is non-nil when this runner is a background subshell
231243 // whose statement may amount to starting one external program;
232244 // the runner reports through it once via [Runner.reportBgStart].
233245 bgStarted chan int
234246
235247 opts runnerOpts
236248
249+ // interactive records the [Interactive] option: whether the runner
250+ // behaves like an interactive shell. Among other things, it decides
251+ // whether background jobs are detached from the statement's context.
252+ interactive bool
253+
237254 origDir string
238255 origParams []string
239256 origOpts runnerOpts
@@ -253,6 +270,11 @@ type Runner struct {
253270 // It is consumed by the enclosing statement once it finishes.
254271 keepRedirs bool
255272
273+ // keptFiles holds the files opened by redirections which were kept
274+ // by "exec", to be closed when the shell or subshell exits.
275+ // TODO: close a file once "exec" replaces it, like other shells do.
276+ keptFiles []io.Closer
277+
256278 // disabledBuiltins holds the names turned off by `enable -n`, which are
257279 // then looked up as external commands like bash does.
258280 disabledBuiltins map [string ]bool
@@ -262,10 +284,6 @@ type Runner struct {
262284 // not read input lines itself, so only a line editor above it knows them.
263285 historyList func () []string
264286 historyClear func ()
265- // keptFiles holds the files opened by redirections which were kept
266- // by "exec", to be closed when the shell or subshell exits.
267- // TODO: close a file once "exec" replaces it, like other shells do.
268- keptFiles []io.Closer
269287
270288 // Fake signal callbacks
271289 callbackErr string
@@ -353,11 +371,30 @@ type bgProc struct {
353371
354372 exit * exitStatus
355373
374+ // id is what $! expands to: a fake PID such as "g1", 1-indexed and
375+ // prefixed to distinguish it from real PIDs on the host operating
376+ // system, or the real process ID once started delivers a non-zero one.
377+ // Read it via [Runner.bgProcID].
378+ id string
379+
380+ // started, when non-nil, delivers the report from [Runner.reportBgStart]:
381+ // the process ID when the background statement started exactly one
382+ // external program, and zero otherwise.
383+ started chan int
384+
356385 // cmd is the source text of the backgrounded statement, as the jobs
357- // builtin prints it. It is empty for the shells behind process
358- // substitutions, which bash does not list as jobs either.
386+ // builtin prints it.
359387 cmd string
360388
389+ // substitution marks the shells behind process substitutions, which bash
390+ // does not list as jobs either. They have no job number, cannot be named
391+ // by a job spec, and a bare wait does not wait for them.
392+ //
393+ // A separate field rather than testing cmd == "": an empty command string
394+ // is a plausible thing to have for other reasons, and every place that
395+ // asked "is this a real job" was really asking this.
396+ substitution bool
397+
361398 // cancel stops the background shell. A job here is a goroutine rather
362399 // than an operating system process, so the kill builtin cancels its
363400 // context instead of sending a signal.
@@ -371,26 +408,6 @@ type bgProc struct {
371408 // wait, as after bash's disown.
372409 disowned bool
373410
374- // id is what $! expands to: a fake PID such as "g1", prefixed to
375- // distinguish it from a real PID on the host operating system, or the
376- // real process ID once started delivers a non-zero one.
377- // Read it via [bgProc.bgProcID].
378- id string
379-
380- // started, when non-nil, delivers the report from [Runner.reportBgStart]:
381- // the process ID when the background statement started exactly one
382- // external program, and zero otherwise.
383- started chan int
384-
385- // substitution marks the shells behind process substitutions, which bash
386- // does not list as jobs either. They have no job number, cannot be named
387- // by a job spec, and a bare wait does not wait for them.
388- //
389- // A separate field rather than testing cmd == "", which is what this used
390- // to do: reaping a job clears its command, and a reaped job must not turn
391- // into a process substitution.
392- substitution bool
393-
394411 // num is the job number that the builtins print and accept, such as the 2
395412 // in `[2]+`. It is assigned when the job starts and never changes, since
396413 // bash leaves the numbers of the other jobs alone when one is reaped.
@@ -443,9 +460,10 @@ func (r *Runner) reapBgProc(bg *bgProc) {
443460 bg .cancel , bg .cmd = nil , ""
444461}
445462
446- // bgProcID returns what $! expands to for this job, waiting for the job's
447- // report first when one is pending; see [Runner.reportBgStart].
448- func (bg * bgProc ) bgProcID () string {
463+ // bgProcID returns what $! expands to for a background job,
464+ // waiting for the job's report first when one is pending;
465+ // see [Runner.reportBgStart].
466+ func (r * Runner ) bgProcID (bg * bgProc ) string {
449467 if bg .started != nil {
450468 if pid := <- bg .started ; pid != 0 {
451469 bg .id = strconv .Itoa (pid )
@@ -460,13 +478,13 @@ func (r *Runner) lookupBgProc(arg string) (*bgProc, bool) {
460478 // Iterate backwards so that, if the OS reused a PID,
461479 // we find the most recent background job.
462480 for _ , bg := range slices .Backward (r .bgProcs ) {
463- if ! bg . reaped && bg . bgProcID () == arg {
481+ if r . bgProcID (bg ) == arg {
464482 return bg , true
465483 }
466484 }
467485 // A reaped job is gone from the table, but bash still answers for the one
468486 // $! names, so that `p=$!; jobs; wait $p` works.
469- if r .lastBg != nil && r .lastBg . bgProcID () == arg {
487+ if r .lastBg != nil && r .bgProcID (r . lastBg ) == arg {
470488 return r .lastBg , true
471489 }
472490 return nil , false
@@ -596,11 +614,14 @@ func History(list func() []string, clear func()) RunnerOption {
596614}
597615
598616// Interactive configures the interpreter to behave like an interactive shell,
599- // akin to Bash. Currently, this only enables the expansion of aliases,
600- // but later on it should also change other behavior.
617+ // akin to Bash. It enables the expansion of aliases, and detaches background
618+ // jobs from the context of the statement that started them, so that a job
619+ // outlives its command line the way it would in an interactive shell; see
620+ // [Runner.StopJobs] for how such jobs end.
601621func Interactive (enabled bool ) RunnerOption {
602622 return func (r * Runner ) error {
603623 r .opts [optExpandAliases ] = enabled
624+ r .interactive = enabled
604625 return nil
605626 }
606627}
@@ -1129,6 +1150,10 @@ func (r *Runner) Reset() {
11291150 // Clean it as we will later do a string prefix match.
11301151 r .tempDir = filepath .Clean (r .tempDir )
11311152 }
1153+ // A detached background job would survive the reset with its cancel func
1154+ // dropped, leaving it running with no way to reach it, so end them all
1155+ // first; a fresh shell has no jobs.
1156+ r .StopJobs (context .Background ())
11321157 // reset the internal state
11331158 * r = Runner {
11341159 umask : 0o022 ,
@@ -1168,6 +1193,8 @@ func (r *Runner) Reset() {
11681193
11691194 dirStack : r .dirStack [:0 ],
11701195 usedNew : r .usedNew ,
1196+
1197+ interactive : r .interactive ,
11711198 }
11721199 // Ensure we stop referencing any pointers before we reuse bgProcs.
11731200 clear (r .bgProcs )
@@ -1320,6 +1347,22 @@ func (r *Runner) Run(ctx context.Context, node syntax.Node) error {
13201347 if ! r .didReset {
13211348 r .Reset ()
13221349 }
1350+ if ! r .inSubshell {
1351+ // Jobs belong to the shell rather than to one statement, and this Run
1352+ // call is the shell for as long as it lasts.
1353+ //
1354+ // An interactive shell runs each command line under its own context,
1355+ // and a job outlives the line that started it. In bash the interrupt
1356+ // which ends a foreground command leaves the background jobs alone. So
1357+ // an interactive runner keeps its jobs across Run calls, and they end
1358+ // via kill, [Runner.StopJobs], or the shell going away. Anywhere else
1359+ // jobs still die with the caller's context, so that an embedder
1360+ // bounding a script with a timeout does not leak them.
1361+ r .jobsBase = ctx
1362+ if r .interactive {
1363+ r .jobsBase = context .WithoutCancel (ctx )
1364+ }
1365+ }
13231366 r .fillExpandConfig (ctx )
13241367 r .exit = exitStatus {}
13251368 r .filename = ""
@@ -1412,8 +1455,6 @@ func (r *Runner) subshell(background bool) *Runner {
14121455 accessHandler : r .accessHandler ,
14131456 procSubstHandler : r .procSubstHandler ,
14141457 procSubsts : r .procSubsts ,
1415- historyList : r .historyList ,
1416- historyClear : r .historyClear ,
14171458 stdin : r .stdin ,
14181459 stdout : r .stdout ,
14191460 stderr : r .stderr ,
@@ -1426,6 +1467,10 @@ func (r *Runner) subshell(background bool) *Runner {
14261467 evalDepth : r .evalDepth ,
14271468 stmtDepth : r .stmtDepth ,
14281469 procSubstUses : slices .Clip (r .procSubstUses ),
1470+ jobsBase : r .jobsBase ,
1471+ inSubshell : true ,
1472+ historyList : r .historyList ,
1473+ historyClear : r .historyClear ,
14291474
14301475 origStdout : r .origStdout , // used for process substitutions
14311476 }
0 commit comments