Skip to content

Commit 88c0b9d

Browse files
easelclaude
andcommitted
fix(daemon): use /proc for daemon process detection on Linux
The isGasTownDaemon function used `ps -p PID -o command=` which doesn't work on Alpine/BusyBox where ps has limited options. This caused daemon startup to fail with "removed stale PID file" errors in container envs. Now uses /proc/PID/cmdline on Linux (more portable), falling back to ps on macOS and other systems where /proc isn't available. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 0cb3160 commit 88c0b9d

1 file changed

Lines changed: 24 additions & 14 deletions

File tree

internal/daemon/daemon.go

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,13 @@ import (
3838
// This is recovery-focused: normal wake is handled by feed subscription (bd activity --follow).
3939
// The daemon is the safety net for dead sessions, GUPP violations, and orphaned work.
4040
type Daemon struct {
41-
config *Config
42-
patrolConfig *DaemonPatrolConfig
43-
tmux *tmux.Tmux
44-
logger *log.Logger
45-
ctx context.Context
46-
cancel context.CancelFunc
47-
curator *feed.Curator
41+
config *Config
42+
patrolConfig *DaemonPatrolConfig
43+
tmux *tmux.Tmux
44+
logger *log.Logger
45+
ctx context.Context
46+
cancel context.CancelFunc
47+
curator *feed.Curator
4848
convoyWatcher *ConvoyWatcher
4949

5050
// Mass death detection: track recent session deaths
@@ -741,16 +741,26 @@ func IsRunning(townRoot string) (bool, int, error) {
741741

742742
// isGasTownDaemon checks if a PID is actually a gt daemon run process.
743743
// This prevents false positives from PID reuse.
744-
// Uses ps command for cross-platform compatibility (Linux, macOS).
744+
// Uses /proc on Linux (works on Alpine/BusyBox), falls back to ps on macOS.
745745
func isGasTownDaemon(pid int) bool {
746-
// Use ps to get command for the PID (works on Linux and macOS)
747-
cmd := exec.Command("ps", "-p", strconv.Itoa(pid), "-o", "command=")
748-
output, err := cmd.Output()
749-
if err != nil {
750-
return false
746+
var cmdline string
747+
748+
// Try /proc first (Linux, including Alpine/BusyBox)
749+
procPath := fmt.Sprintf("/proc/%d/cmdline", pid)
750+
if data, err := os.ReadFile(procPath); err == nil {
751+
// cmdline has null-separated args, replace with spaces
752+
cmdline = strings.ReplaceAll(string(data), "\x00", " ")
753+
} else {
754+
// Fall back to ps (macOS, other Unix)
755+
cmd := exec.Command("ps", "-p", strconv.Itoa(pid), "-o", "command=")
756+
output, err := cmd.Output()
757+
if err != nil {
758+
return false
759+
}
760+
cmdline = string(output)
751761
}
752762

753-
cmdline := strings.TrimSpace(string(output))
763+
cmdline = strings.TrimSpace(cmdline)
754764

755765
// Check if it's "gt daemon run" or "/path/to/gt daemon run"
756766
return strings.Contains(cmdline, "gt") && strings.Contains(cmdline, "daemon") && strings.Contains(cmdline, "run")

0 commit comments

Comments
 (0)