Files
pmg/internal/pty/foreground_unix.go
T
9714a6f4c2 fix(pty): treat background jobs as non-interactive to avoid SIGTTOU stop (#324)
* fix(pty): treat background jobs as non-interactive to avoid SIGTTOU stop

IsInteractiveTerminal only checked that stdin/stdout are TTYs. A background
job (pmg npm run test &) still has the TTY on stdin/stdout, so pmg picked
PTY mode and called tcsetattr to enter raw mode. Changing terminal modes
from a background process group makes the kernel stop the process with
SIGTTOU, leaving the job hanging in Stopped state.

Check that the process group is the terminal's foreground process group
(tcgetpgrp == getpgrp) before treating the terminal as interactive, so
background jobs fall through to direct execution.

Fixes #322

https://claude.ai/code/session_01PBBo5CKkzg68MMGrgCTQcY

* test(pty): fail on output copy timeout to avoid racy buffer read

Reading the output buffer after a silent select timeout races with the
io.Copy goroutine still writing to it. Fail the test on timeout instead.

Also fix a grammar nit in the IsInteractiveTerminal doc comment.

https://claude.ai/code/session_01PBBo5CKkzg68MMGrgCTQcY

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-10 09:57:08 +05:30

22 lines
588 B
Go

//go:build !windows
package pty
import (
"golang.org/x/sys/unix"
)
// isForegroundProcess reports whether this process belongs to the terminal's
// foreground process group. A background job (e.g. `pmg npm install &`) still
// has the TTY on stdin/stdout, but changing terminal modes or reading from
// the TTY in a background process group triggers SIGTTOU/SIGTTIN, which stops
// the process.
func isForegroundProcess(fd uintptr) bool {
foregroundPgrp, err := unix.IoctlGetInt(int(fd), unix.TIOCGPGRP)
if err != nil {
return false
}
return foregroundPgrp == unix.Getpgrp()
}