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>
This commit is contained in:
Abhisek Datta
2026-06-10 09:57:08 +05:30
committed by GitHub
co-authored by Claude
parent 141894ed8f
commit 9714a6f4c2
4 changed files with 149 additions and 4 deletions
+6 -4
View File
@@ -42,9 +42,11 @@ type InteractiveSession interface {
Close() error
}
// IsInteractiveTerminal returns true if stdin is a real terminal (TTY).
// Returns false in CI environments (when the "CI" env var set to "true"),
// when input or output is piped, or in non-interactive shells.
// IsInteractiveTerminal returns true if stdin is a real terminal (TTY) and
// the process can safely drive it. Returns false in CI environments (when the
// "CI" env var is set to "true"), when input or output is piped, in
// non-interactive shells, or when running as a background job (where putting
// the terminal into raw mode would stop the process with SIGTTOU).
func IsInteractiveTerminal() bool {
if ci := os.Getenv("CI"); ci != "" && strings.ToLower(ci) == "true" {
return false
@@ -58,7 +60,7 @@ func IsInteractiveTerminal() bool {
return false
}
return true
return isForegroundProcess(os.Stdin.Fd())
}
var _ InteractiveSession = &session{}