From 66805079dea871fd8c2e9733ee3709a6f8a5feec Mon Sep 17 00:00:00 2001 From: dustin Date: Wed, 14 Jan 2026 04:28:16 +0700 Subject: [PATCH] fix: prevent gt down --all from respawning bd daemon (#457) CountBdDaemons() was using `bd daemon list --json` which triggers daemon auto-start as a side effect. During shutdown verification, this caused a new daemon to spawn after all daemons were killed, resulting in "bd daemon shutdown incomplete: 1 still running" error. Replaced all `bd daemon killall` calls with pkill in: - stopBdDaemons() - restartBdDaemons() Changed CountBdDaemons() to use pgrep instead of bd daemon list. Also removed the now-unused parseBdDaemonCount helper function and its tests. --- internal/beads/daemon.go | 54 +++++++++-------------------------- internal/beads/daemon_test.go | 40 -------------------------- 2 files changed, 13 insertions(+), 81 deletions(-) diff --git a/internal/beads/daemon.go b/internal/beads/daemon.go index 93a7a8d6..f4b979ff 100644 --- a/internal/beads/daemon.go +++ b/internal/beads/daemon.go @@ -109,9 +109,8 @@ func EnsureBdDaemonHealth(workDir string) string { // restartBdDaemons restarts all bd daemons. func restartBdDaemons() error { //nolint:unparam // error return kept for future use - // Stop all daemons first - stopCmd := exec.Command("bd", "daemon", "killall") - _ = stopCmd.Run() // Ignore errors - daemons might not be running + // Stop all daemons first using pkill to avoid auto-start side effects + _ = exec.Command("pkill", "-TERM", "-f", "bd daemon").Run() // Give time for cleanup time.Sleep(200 * time.Millisecond) @@ -159,39 +158,20 @@ func StopAllBdProcesses(dryRun, force bool) (int, int, error) { } // CountBdDaemons returns count of running bd daemons. +// Uses pgrep instead of "bd daemon list" to avoid triggering daemon auto-start +// during shutdown verification. func CountBdDaemons() int { - listCmd := exec.Command("bd", "daemon", "list", "--json") - output, err := listCmd.Output() + // Use pgrep -f with wc -l for cross-platform compatibility + // (macOS pgrep doesn't support -c flag) + cmd := exec.Command("sh", "-c", "pgrep -f 'bd daemon' 2>/dev/null | wc -l") + output, err := cmd.Output() if err != nil { return 0 } - return parseBdDaemonCount(output) + count, _ := strconv.Atoi(strings.TrimSpace(string(output))) + return count } -// parseBdDaemonCount parses bd daemon list --json output. -func parseBdDaemonCount(output []byte) int { - if len(output) == 0 { - return 0 - } - - var daemons []any - if err := json.Unmarshal(output, &daemons); err == nil { - return len(daemons) - } - - var wrapper struct { - Daemons []any `json:"daemons"` - Count int `json:"count"` - } - if err := json.Unmarshal(output, &wrapper); err == nil { - if wrapper.Count > 0 { - return wrapper.Count - } - return len(wrapper.Daemons) - } - - return 0 -} func stopBdDaemons(force bool) (int, int) { before := CountBdDaemons() @@ -199,19 +179,11 @@ func stopBdDaemons(force bool) (int, int) { return 0, 0 } - killCmd := exec.Command("bd", "daemon", "killall") - _ = killCmd.Run() - - time.Sleep(100 * time.Millisecond) - - after := CountBdDaemons() - if after == 0 { - return before, 0 - } - + // Use pkill directly instead of "bd daemon killall" to avoid triggering + // daemon auto-start as a side effect of running bd commands. // Note: pkill -f pattern may match unintended processes in rare cases // (e.g., editors with "bd daemon" in file content). This is acceptable - // as a fallback when bd daemon killall fails. + // given the alternative of respawning daemons during shutdown. if force { _ = exec.Command("pkill", "-9", "-f", "bd daemon").Run() } else { diff --git a/internal/beads/daemon_test.go b/internal/beads/daemon_test.go index 80c2762b..5cc0de5e 100644 --- a/internal/beads/daemon_test.go +++ b/internal/beads/daemon_test.go @@ -5,46 +5,6 @@ import ( "testing" ) -func TestParseBdDaemonCount_Array(t *testing.T) { - input := []byte(`[{"pid":1234},{"pid":5678}]`) - count := parseBdDaemonCount(input) - if count != 2 { - t.Errorf("expected 2, got %d", count) - } -} - -func TestParseBdDaemonCount_ObjectWithCount(t *testing.T) { - input := []byte(`{"count":3,"daemons":[{},{},{}]}`) - count := parseBdDaemonCount(input) - if count != 3 { - t.Errorf("expected 3, got %d", count) - } -} - -func TestParseBdDaemonCount_ObjectWithDaemons(t *testing.T) { - input := []byte(`{"daemons":[{},{}]}`) - count := parseBdDaemonCount(input) - if count != 2 { - t.Errorf("expected 2, got %d", count) - } -} - -func TestParseBdDaemonCount_Empty(t *testing.T) { - input := []byte(``) - count := parseBdDaemonCount(input) - if count != 0 { - t.Errorf("expected 0, got %d", count) - } -} - -func TestParseBdDaemonCount_Invalid(t *testing.T) { - input := []byte(`not json`) - count := parseBdDaemonCount(input) - if count != 0 { - t.Errorf("expected 0 for invalid JSON, got %d", count) - } -} - func TestCountBdActivityProcesses(t *testing.T) { count := CountBdActivityProcesses() if count < 0 {