From c299f44aea5351a3c5f46ce2e47ef5b39ccd55a4 Mon Sep 17 00:00:00 2001 From: PV Date: Sun, 4 Jan 2026 00:17:37 -0800 Subject: [PATCH] refactor(session): extract town session helpers for DRY shutdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, town-level session stopping (Mayor, Boot, Deacon) was implemented inline in gt down with separate code blocks for each session. The shutdown order (Boot must stop before Deacon to prevent the watchdog from restarting Deacon) was implicit in the code ordering. Add session.TownSessions() and session.StopTownSession() to centralize town-level session management. This provides a single source of truth for the session list, shutdown order, and graceful/force logic. Refactor gt down to use these helpers instead of inline logic. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- internal/cmd/down.go | 43 +++++++++++--------------------- internal/session/town.go | 53 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 29 deletions(-) create mode 100644 internal/session/town.go diff --git a/internal/cmd/down.go b/internal/cmd/down.go index 63b3cc7c..41e47953 100644 --- a/internal/cmd/down.go +++ b/internal/cmd/down.go @@ -5,9 +5,9 @@ import ( "time" "github.com/spf13/cobra" - "github.com/steveyegge/gastown/internal/boot" "github.com/steveyegge/gastown/internal/daemon" "github.com/steveyegge/gastown/internal/events" + "github.com/steveyegge/gastown/internal/session" "github.com/steveyegge/gastown/internal/style" "github.com/steveyegge/gastown/internal/tmux" "github.com/steveyegge/gastown/internal/workspace" @@ -73,35 +73,20 @@ func runDown(cmd *cobra.Command, args []string) error { } } - // Get session names - mayorSession := getMayorSessionName() - deaconSession := getDeaconSessionName() - - // 2. Stop Mayor - if err := stopSession(t, mayorSession); err != nil { - printDownStatus("Mayor", false, err.Error()) - allOK = false - } else { - printDownStatus("Mayor", true, "stopped") + // 2. Stop town-level sessions (Mayor, Boot, Deacon) in correct order + for _, ts := range session.TownSessions() { + stopped, err := session.StopTownSession(t, ts, downForce) + if err != nil { + printDownStatus(ts.Name, false, err.Error()) + allOK = false + } else if stopped { + printDownStatus(ts.Name, true, "stopped") + } else { + printDownStatus(ts.Name, true, "not running") + } } - // 3. Stop Boot (Deacon's watchdog) - if err := stopSession(t, boot.SessionName); err != nil { - printDownStatus("Boot", false, err.Error()) - allOK = false - } else { - printDownStatus("Boot", true, "stopped") - } - - // 4. Stop Deacon - if err := stopSession(t, deaconSession); err != nil { - printDownStatus("Deacon", false, err.Error()) - allOK = false - } else { - printDownStatus("Deacon", true, "stopped") - } - - // 5. Stop Daemon last + // 3. Stop Daemon last running, _, _ := daemon.IsRunning(townRoot) if running { if err := daemon.StopDaemon(townRoot); err != nil { @@ -114,7 +99,7 @@ func runDown(cmd *cobra.Command, args []string) error { printDownStatus("Daemon", true, "not running") } - // 6. Kill tmux server if --all + // 4. Kill tmux server if --all if downAll { if err := t.KillServer(); err != nil { printDownStatus("Tmux server", false, err.Error()) diff --git a/internal/session/town.go b/internal/session/town.go new file mode 100644 index 00000000..05fdd44d --- /dev/null +++ b/internal/session/town.go @@ -0,0 +1,53 @@ +// Package session provides polecat session lifecycle management. +package session + +import ( + "fmt" + "time" + + "github.com/steveyegge/gastown/internal/boot" + "github.com/steveyegge/gastown/internal/tmux" +) + +// TownSession represents a town-level tmux session. +type TownSession struct { + Name string // Display name (e.g., "Mayor") + SessionID string // Tmux session ID (e.g., "gt-mayor") +} + +// TownSessions returns the list of town-level sessions in shutdown order. +// Order matters: Boot (Deacon's watchdog) must be stopped before Deacon, +// otherwise Boot will try to restart Deacon. +func TownSessions() []TownSession { + return []TownSession{ + {"Mayor", MayorSessionName()}, + {"Boot", boot.SessionName}, + {"Deacon", DeaconSessionName()}, + } +} + +// StopTownSession stops a single town-level tmux session. +// If force is true, skips graceful shutdown (Ctrl-C) and kills immediately. +// Returns true if the session was running and stopped, false if not running. +func StopTownSession(t *tmux.Tmux, ts TownSession, force bool) (bool, error) { + running, err := t.HasSession(ts.SessionID) + if err != nil { + return false, err + } + if !running { + return false, nil + } + + // Try graceful shutdown first (unless forced) + if !force { + _ = t.SendKeysRaw(ts.SessionID, "C-c") + time.Sleep(100 * time.Millisecond) + } + + // Kill the session + if err := t.KillSession(ts.SessionID); err != nil { + return false, fmt.Errorf("killing %s session: %w", ts.Name, err) + } + + return true, nil +}