Symptom
Running graff --yolo (or the TUI generally) sometimes dies immediately with:
zsh: suspended (tty output) graff --yolo
Reproducible by racing two graff instances on one terminal - e.g. /resume-ing the same session from a second shell, or the GUI's graff sidecar starting while a CLI graff holds the terminal. Whichever instance touches termios second gets stopped.
Root cause
src/term.zig:144, in tty.enterRaw():
std.posix.tcsetattr(fd, .NOW, raw) catch return null;
POSIX: when a process in a background process group calls tcsetattr() on its controlling terminal, the kernel sends it SIGTTOU, whose default disposition is stop the process. This fires regardless of the terminal's tostop flag - tcsetattr from a background pgrp always raises it.
A terminal has exactly one foreground process group. Two graff instances sharing one tty means the second is background by definition, so it is stopped the instant it enters raw mode.
There is currently no SIGTTOU handling and no tcgetpgrp foreground check anywhere in the tree. tty.restore() (src/term.zig:155) has the same exposure on the way out.
Fix
- Ignore
SIGTTOU around the tcsetattr calls in both enterRaw() and restore(). Save the prior disposition, install SIG_IGN, call, restore. The call then succeeds instead of stopping the process - correct here, since graff only ever sets its own terminal state. This alone removes the symptom.
- Foreground check at startup: if
tcgetpgrp(STDIN_FILENO) != getpgrp(), graff is a background job. Emit a clear message (graff needs the foreground - run fg) or degrade to non-TUI mode, instead of silently suspending.
- Re-enter raw mode on
SIGCONT, so a suspended-then-fg'd graff is not left with a broken terminal mode.
- Lock the session file. Two instances on one
--resume / /resume session is a corruption race independent of the tty. Take an exclusive advisory lock on <name>.session.json; the second instance should report "session open in another graff" rather than racing. Same cross-process-lock pattern the OAuth credential writes need.
Notes
- Windows is unaffected (console API, no process groups).
- Zig 0.16: verify
tcgetpgrp is exposed in std; may need ioctl(TIOCGPGRP) directly, same situation as chdir.
- Items 1-3 are the tty fix. Item 4 is a separate concurrency bug that shares the repro and should probably be its own PR.
Symptom
Running
graff --yolo(or the TUI generally) sometimes dies immediately with:Reproducible by racing two graff instances on one terminal - e.g.
/resume-ing the same session from a second shell, or the GUI's graff sidecar starting while a CLI graff holds the terminal. Whichever instance touches termios second gets stopped.Root cause
src/term.zig:144, intty.enterRaw():POSIX: when a process in a background process group calls
tcsetattr()on its controlling terminal, the kernel sends itSIGTTOU, whose default disposition is stop the process. This fires regardless of the terminal'stostopflag -tcsetattrfrom a background pgrp always raises it.A terminal has exactly one foreground process group. Two graff instances sharing one tty means the second is background by definition, so it is stopped the instant it enters raw mode.
There is currently no
SIGTTOUhandling and notcgetpgrpforeground check anywhere in the tree.tty.restore()(src/term.zig:155) has the same exposure on the way out.Fix
SIGTTOUaround thetcsetattrcalls in bothenterRaw()andrestore(). Save the prior disposition, installSIG_IGN, call, restore. The call then succeeds instead of stopping the process - correct here, since graff only ever sets its own terminal state. This alone removes the symptom.tcgetpgrp(STDIN_FILENO) != getpgrp(), graff is a background job. Emit a clear message (graff needs the foreground - runfg) or degrade to non-TUI mode, instead of silently suspending.SIGCONT, so a suspended-then-fg'd graff is not left with a broken terminal mode.--resume//resumesession is a corruption race independent of the tty. Take an exclusive advisory lock on<name>.session.json; the second instance should report "session open in another graff" rather than racing. Same cross-process-lock pattern the OAuth credential writes need.Notes
tcgetpgrpis exposed in std; may needioctl(TIOCGPGRP)directly, same situation aschdir.