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 <noreply@anthropic.com>
162 lines
4.3 KiB
Go
162 lines
4.3 KiB
Go
package cmd
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
"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"
|
|
)
|
|
|
|
var downCmd = &cobra.Command{
|
|
Use: "down",
|
|
GroupID: GroupServices,
|
|
Short: "Stop all Gas Town services",
|
|
Long: `Stop all Gas Town long-lived services.
|
|
|
|
This gracefully shuts down all infrastructure agents:
|
|
|
|
• Witnesses - Per-rig polecat managers
|
|
• Mayor - Global work coordinator
|
|
• Boot - Deacon's watchdog
|
|
• Deacon - Health orchestrator
|
|
• Daemon - Go background process
|
|
|
|
Polecats are NOT stopped by this command - use 'gt swarm stop' or
|
|
kill individual polecats with 'gt polecat kill'.
|
|
|
|
This is useful for:
|
|
• Taking a break (stop token consumption)
|
|
• Clean shutdown before system maintenance
|
|
• Resetting the town to a clean state`,
|
|
RunE: runDown,
|
|
}
|
|
|
|
var (
|
|
downQuiet bool
|
|
downForce bool
|
|
downAll bool
|
|
)
|
|
|
|
func init() {
|
|
downCmd.Flags().BoolVarP(&downQuiet, "quiet", "q", false, "Only show errors")
|
|
downCmd.Flags().BoolVarP(&downForce, "force", "f", false, "Force kill without graceful shutdown")
|
|
downCmd.Flags().BoolVarP(&downAll, "all", "a", false, "Also kill the tmux server")
|
|
rootCmd.AddCommand(downCmd)
|
|
}
|
|
|
|
func runDown(cmd *cobra.Command, args []string) error {
|
|
townRoot, err := workspace.FindFromCwdOrError()
|
|
if err != nil {
|
|
return fmt.Errorf("not in a Gas Town workspace: %w", err)
|
|
}
|
|
|
|
t := tmux.NewTmux()
|
|
allOK := true
|
|
|
|
// Stop in reverse order of startup
|
|
|
|
// 1. Stop witnesses first
|
|
rigs := discoverRigs(townRoot)
|
|
for _, rigName := range rigs {
|
|
sessionName := fmt.Sprintf("gt-%s-witness", rigName)
|
|
if err := stopSession(t, sessionName); err != nil {
|
|
printDownStatus(fmt.Sprintf("Witness (%s)", rigName), false, err.Error())
|
|
allOK = false
|
|
} else {
|
|
printDownStatus(fmt.Sprintf("Witness (%s)", rigName), 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 Daemon last
|
|
running, _, _ := daemon.IsRunning(townRoot)
|
|
if running {
|
|
if err := daemon.StopDaemon(townRoot); err != nil {
|
|
printDownStatus("Daemon", false, err.Error())
|
|
allOK = false
|
|
} else {
|
|
printDownStatus("Daemon", true, "stopped")
|
|
}
|
|
} else {
|
|
printDownStatus("Daemon", true, "not running")
|
|
}
|
|
|
|
// 4. Kill tmux server if --all
|
|
if downAll {
|
|
if err := t.KillServer(); err != nil {
|
|
printDownStatus("Tmux server", false, err.Error())
|
|
allOK = false
|
|
} else {
|
|
printDownStatus("Tmux server", true, "killed")
|
|
}
|
|
}
|
|
|
|
fmt.Println()
|
|
if allOK {
|
|
fmt.Printf("%s All services stopped\n", style.Bold.Render("✓"))
|
|
// Log halt event with stopped services
|
|
stoppedServices := []string{"daemon", "deacon", "boot", "mayor"}
|
|
for _, rigName := range rigs {
|
|
stoppedServices = append(stoppedServices, fmt.Sprintf("%s/witness", rigName))
|
|
}
|
|
if downAll {
|
|
stoppedServices = append(stoppedServices, "tmux-server")
|
|
}
|
|
_ = events.LogFeed(events.TypeHalt, "gt", events.HaltPayload(stoppedServices))
|
|
} else {
|
|
fmt.Printf("%s Some services failed to stop\n", style.Bold.Render("✗"))
|
|
return fmt.Errorf("not all services stopped")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func printDownStatus(name string, ok bool, detail string) {
|
|
if downQuiet && ok {
|
|
return
|
|
}
|
|
if ok {
|
|
fmt.Printf("%s %s: %s\n", style.SuccessPrefix, name, style.Dim.Render(detail))
|
|
} else {
|
|
fmt.Printf("%s %s: %s\n", style.ErrorPrefix, name, detail)
|
|
}
|
|
}
|
|
|
|
// stopSession gracefully stops a tmux session.
|
|
func stopSession(t *tmux.Tmux, sessionName string) error {
|
|
running, err := t.HasSession(sessionName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !running {
|
|
return nil // Already stopped
|
|
}
|
|
|
|
// Try graceful shutdown first (Ctrl-C, best-effort interrupt)
|
|
if !downForce {
|
|
_ = t.SendKeysRaw(sessionName, "C-c")
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
|
|
// Kill the session
|
|
return t.KillSession(sessionName)
|
|
}
|