diff --git a/README.md b/README.md index 124c488..9fb3227 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,7 @@ This creates two binaries in `bin/`: |---------|-------------| | `map up [-f]` | Start the daemon (foreground with -f) | | `map down [-f]` | Stop the daemon (force immediate shutdown with -f) | +| `map clean` | Clean up orphaned processes, tmux sessions, and socket files | | `map watch` | Stream real-time events from the daemon | ### Agent Management diff --git a/internal/cli/clean.go b/internal/cli/clean.go new file mode 100644 index 0000000..e762904 --- /dev/null +++ b/internal/cli/clean.go @@ -0,0 +1,125 @@ +package cli + +import ( + "fmt" + "os" + "os/exec" + "strconv" + "strings" + + "github.com/pmarsceill/mapcli/internal/daemon" + "github.com/spf13/cobra" +) + +var cleanCmd = &cobra.Command{ + Use: "clean", + Short: "Clean up orphaned processes and resources", + Long: `Clean up orphaned mapd processes, tmux sessions, and socket files. + +This is useful when the daemon didn't shut down cleanly and left behind +stale processes or socket files that prevent starting a new daemon.`, + RunE: runClean, +} + +func init() { + rootCmd.AddCommand(cleanCmd) +} + +func runClean(cmd *cobra.Command, args []string) error { + var cleaned bool + + // 1. Kill orphaned mapd/map processes + killedProcs, err := killOrphanedProcesses() + if err != nil { + fmt.Printf("warning: error killing processes: %v\n", err) + } + if killedProcs > 0 { + fmt.Printf("killed %d orphaned process(es)\n", killedProcs) + cleaned = true + } + + // 2. Kill orphaned tmux sessions + killedSessions, err := killOrphanedTmuxSessions() + if err != nil { + fmt.Printf("warning: error killing tmux sessions: %v\n", err) + } + if killedSessions > 0 { + fmt.Printf("killed %d orphaned tmux session(s)\n", killedSessions) + cleaned = true + } + + // 3. Remove socket file if it exists + if _, err := os.Stat(socketPath); err == nil { + if err := os.Remove(socketPath); err != nil { + fmt.Printf("warning: failed to remove socket %s: %v\n", socketPath, err) + } else { + fmt.Printf("removed socket %s\n", socketPath) + cleaned = true + } + } + + if !cleaned { + fmt.Println("nothing to clean") + } + + return nil +} + +// killOrphanedProcesses finds and kills mapd and map processes +func killOrphanedProcesses() (int, error) { + // Get current process ID to avoid killing ourselves + currentPID := os.Getpid() + + // Find mapd and map processes using pgrep + output, err := exec.Command("pgrep", "-f", "mapd|map up").Output() + if err != nil { + // pgrep returns exit code 1 when no processes found + if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 { + return 0, nil + } + return 0, err + } + + var killed int + for line := range strings.SplitSeq(strings.TrimSpace(string(output)), "\n") { + if line == "" { + continue + } + pid, err := strconv.Atoi(line) + if err != nil { + continue + } + // Don't kill ourselves + if pid == currentPID { + continue + } + // Kill the process + proc, err := os.FindProcess(pid) + if err != nil { + continue + } + if err := proc.Kill(); err == nil { + killed++ + } + } + + return killed, nil +} + +// killOrphanedTmuxSessions kills map-agent-* tmux sessions +func killOrphanedTmuxSessions() (int, error) { + sessions, err := daemon.ListTmuxSessions() + if err != nil { + return 0, err + } + + var killed int + for _, session := range sessions { + cmd := exec.Command("tmux", "kill-session", "-t", session) + if err := cmd.Run(); err == nil { + killed++ + } + } + + return killed, nil +}