[process][freebsd]: Terminal() fails instead of returning "" when no pty is open
Splitting this out of the CI PR as you suggested.
On FreeBSD, devfs materialises /dev/pts only while a pty is open. When none is, getTerminalMap returns an error, and TerminalWithContext turns that into a failure for every process -- including ones attached to a real console terminal, because the /dev/tty* entries it had already collected are thrown away on the way out.
process/process_posix.go:41-55
for _, devname := range devnames {
if strings.HasPrefix(devname, "tty") {
termfiles = append(termfiles, filepath.Join(devPath, devname))
}
}
var ptsnames []string
ptsPath := filepath.Join(devPath, "pts")
ptsd, err := os.Open(ptsPath)
if err != nil {
ptsnames, _ = filepath.Glob(filepath.Join(devPath, "ttyp*"))
if ptsnames == nil {
return nil, err
}
termfiles = append(termfiles, ptsnames...)
} else {termfiles is already populated at that point. Modern FreeBSD has no /dev/ttyp*, so the glob is empty and the function returns nil with everything discarded.
Two jobs on FreeBSD 15.1, identical except that one holds a pty open with
script -q /dev/null sleep 600 &:
no pty open open /dev/pts -> no such file or directory /dev/tty* -> 15 entries: ttyu0 ttyu0.init ttyu0.lock ttyv0 ttyv1 ... ttyvb /dev/ttyp* -> 0 getTerminalMap -> entries=0 err=open /dev/pts: no such file or directory Terminal() -> "" err=open /dev/pts: no such file or directory
one pty held open open /dev/pts -> ok /dev/tty* -> the same 15 entries /dev/ttyp* -> 0 getTerminalMap -> entries=16 err= Terminal() -> "" err=
https://github.com/neilpang/gopsutil/actions/runs/31376838154
The second column shows the answer this process should get: "" with no error, since it has no controlling terminal. Without a pty it gets the same "" but with an error attached, so a caller cannot tell "no terminal" from "lookup failed". A process that does own /dev/ttyv0 would be misreported the same way, since those 15 entries never reach the map.
This is not specific to CI. On a local FreeBSD 15.1 VM, driven over a non-interactive ssh session with nothing holding a pty:
ls -d /dev/pts -> No such file or directory
ls -d /dev/tty* -> ttyu0 ttyu0.init ttyu0.lock ttyv0 ... ttyvb
mount -t devfs devfs /tmp/probedev
ls -d /tmp/probedev/pts -> No such file or directoryand once a pty is held, /dev/pts appears with a single entry, 0. So the trigger is simply that no pty is open on the machine at that moment, which is the normal state of a headless box being driven non-interactively.
One correction to the jail example from the review: a jail is not a separate case. Its devfs shows no tty entries at all under the default ruleset, and /dev/pts appears inside it as soon as the host opens one -- checked with a real jail (jls confirming, devfs mounted) on that same VM.
Linux does not hit any of this because devpts is always mounted, so the branch is never taken there.
A minimal fix would be to keep what was collected instead of discarding it: /dev/pts being absent is a normal FreeBSD state, not a failure to read /dev. The genuine error case -- /dev itself unreadable -- is already handled earlier at lines 31-34. Happy to send that patch if you would like it shaped that way.
Worth noting that process/process_posix_test.go:24-26 already skips when getTerminalMap errors, while process_test.go's TestTerminal asserts NoError -- so the suite half expects this already.
Written with the help of Claude; I read and tested all of it.
Source: shirou/gopsutil