Compare commits

...
1 Commits
Author SHA1 Message Date
Florian WaltherandClaude Opus 5.5 da281ca4ab fix job control on FreeBSD and macOS: do not use WCONTINUED
Go's syscall.WaitStatus decodes the "continued" status wrongly on BSD:
on FreeBSD it is read as killed by SIGCONT, so after bg the job was
reported as "Exit 147" and removed from the job table while it was
still running (fg: "no such job"); on macOS it is read as stopped.

The shell marks jobs as running itself when it continues them, so wait4
is called without WCONTINUED and only exited/signaled statuses finish a
process.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 23:35:25 +02:00
3 changed files with 42 additions and 8 deletions

No files matched your search

+1
View File
@@ -256,6 +256,7 @@ $ bg %2 # oder: angehaltenen Job im Hintergrund weiterlauf
- `exit` (oder Ctrl+D) mit angehaltenen Jobs warnt einmal (`there are stopped jobs`), ein zweites `exit` direkt danach beendet gosh. - `exit` (oder Ctrl+D) mit angehaltenen Jobs warnt einmal (`there are stopped jobs`), ein zweites `exit` direkt danach beendet gosh.
- `cd`, `set`, `export`, `exit`, `source`, `fg`, `bg` und `wait` lassen sich nicht mit `&` im Hintergrund starten. - `cd`, `set`, `export`, `exit`, `source`, `fg`, `bg` und `wait` lassen sich nicht mit `&` im Hintergrund starten.
- In Skripten und mit `gosh -c` gibt es keine Job-Control (kein Ctrl+Z, kein `fg`/`bg`), `&` und `wait` funktionieren aber. Hintergrund-Jobs lesen dort von `/dev/null`. - In Skripten und mit `gosh -c` gibt es keine Job-Control (kein Ctrl+Z, kein `fg`/`bg`), `&` und `wait` funktionieren aber. Hintergrund-Jobs lesen dort von `/dev/null`.
- Wird ein angehaltener Job von außen fortgesetzt (`kill -CONT`), zeigt `jobs` ihn weiter als `Stopped`, bis er endet oder erneut angehalten wird; `fg` und `bg` funktionieren trotzdem.
- Einen Job beenden lässt sich mit `kill` über die Prozess-ID (z. B. `kill 12345`); `kill %1` versteht gosh noch nicht. - Einen Job beenden lässt sich mit `kill` über die Prozess-ID (z. B. `kill 12345`); `kill %1` versteht gosh noch nicht.
### Farben ### Farben
+19 -8
View File
@@ -162,9 +162,7 @@ func (j *job) update(pid int, ws syscall.WaitStatus) {
switch { switch {
case ws.Stopped(): case ws.Stopped():
p.stopped = true p.stopped = true
case ws.Continued(): case ws.Exited() || ws.Signaled():
p.stopped = false
default:
p.finished, p.stopped = true, false p.finished, p.stopped = true, false
p.err = waitStatusError(ws) p.err = waitStatusError(ws)
p.cmd.Process.Release() p.cmd.Process.Release()
@@ -185,17 +183,30 @@ func waitStatusError(ws syscall.WaitStatus) error {
return nil return nil
} }
// wait waits until no process of the job is running any more (all finished // waitFlags returns the options for wait4: WUNTRACED to see stopped
// or, with job control, stopped). With nohang it only collects the status // processes (with job control) and WNOHANG to not block.
// changes that are already available and does not block. //
func (j *job) wait(nohang bool) { // WCONTINUED is not used: Go's syscall.WaitStatus decodes the "continued"
// status wrongly on FreeBSD (as killed by SIGCONT) and macOS (as stopped).
// The shell marks jobs as running itself when it continues them (fg, bg); a
// job continued from outside (kill -CONT) stays "Stopped" in the job table
// until it stops again or exits.
func waitFlags(j *job, nohang bool) int {
flags := 0 flags := 0
if jobControl && j.pgid != 0 { if jobControl && j.pgid != 0 {
flags |= syscall.WUNTRACED | syscall.WCONTINUED flags |= syscall.WUNTRACED
} }
if nohang { if nohang {
flags |= syscall.WNOHANG flags |= syscall.WNOHANG
} }
return flags
}
// wait waits until no process of the job is running any more (all finished
// or, with job control, stopped). With nohang it only collects the status
// changes that are already available and does not block.
func (j *job) wait(nohang bool) {
flags := waitFlags(j, nohang)
for { for {
// With job control all processes of the job are in its process // With job control all processes of the job are in its process
+22
View File
@@ -105,6 +105,21 @@ func (s *ptyShell) expect(want string) {
s.t.Fatalf("timeout waiting for %q, output:\n%s", want, s.out.String()) s.t.Fatalf("timeout waiting for %q, output:\n%s", want, s.out.String())
} }
func TestWaitFlags_NoWCONTINUED(t *testing.T) {
defer func(saved bool) { jobControl = saved }(jobControl)
jobControl = true
j := &job{pgid: 1}
for _, nohang := range []bool{false, true} {
flags := waitFlags(j, nohang)
if flags&syscall.WCONTINUED != 0 {
t.Errorf("waitFlags(nohang=%v) contains WCONTINUED, which Go decodes wrongly on BSD", nohang)
}
if flags&syscall.WUNTRACED == 0 {
t.Errorf("waitFlags(nohang=%v) misses WUNTRACED", nohang)
}
}
}
func TestJobControl(t *testing.T) { func TestJobControl(t *testing.T) {
s := startPtyShell(t) s := startPtyShell(t)
@@ -120,6 +135,13 @@ func TestJobControl(t *testing.T) {
s.send("bg\n") s.send("bg\n")
s.expect("[1]+ Running sleep 30 &") s.expect("[1]+ Running sleep 30 &")
// After bg the job keeps running and is not reported as finished
// (regression: on FreeBSD the "continued" status was read as killed by
// SIGCONT, "Exit 147")
time.Sleep(300 * time.Millisecond)
s.send("jobs\n")
s.expect("[1]+ Running sleep 30 &")
// fg brings it back, Ctrl+C reaches only the job // fg brings it back, Ctrl+C reaches only the job
s.send("fg %1\n") s.send("fg %1\n")
s.expect("sleep 30") s.expect("sleep 30")