From 56f09677ca2dbc9348c4214fefe6bc4d1dc19d34 Mon Sep 17 00:00:00 2001 From: dacrab Date: Fri, 27 Dec 2024 13:53:36 +0200 Subject: [PATCH] feat: improve process management for auto-closing Cursor instances --- internal/process/manager.go | 176 ++++++++++++++++++------------------ 1 file changed, 87 insertions(+), 89 deletions(-) diff --git a/internal/process/manager.go b/internal/process/manager.go index 3d29ee9..00d568a 100644 --- a/internal/process/manager.go +++ b/internal/process/manager.go @@ -1,12 +1,10 @@ package process import ( - "context" "fmt" "os/exec" "runtime" "strings" - "sync" "time" "github.com/sirupsen/logrus" @@ -14,17 +12,23 @@ import ( // Config holds process manager configuration type Config struct { - RetryAttempts int - RetryDelay time.Duration - Timeout time.Duration + MaxAttempts int + RetryDelay time.Duration + ProcessPatterns []string } // DefaultConfig returns the default configuration func DefaultConfig() *Config { return &Config{ - RetryAttempts: 3, - RetryDelay: time.Second, - Timeout: 30 * time.Second, + MaxAttempts: 3, + RetryDelay: time.Second, + ProcessPatterns: []string{ + "Cursor.exe", // Windows + "Cursor", // Linux/macOS binary + "cursor", // Linux/macOS process + "cursor-helper", // Helper process + "cursor-id-modifier", // Our tool + }, } } @@ -32,7 +36,6 @@ func DefaultConfig() *Config { type Manager struct { config *Config log *logrus.Logger - mu sync.Mutex } // NewManager creates a new process manager @@ -49,114 +52,109 @@ func NewManager(config *Config, log *logrus.Logger) *Manager { } } -// KillCursorProcesses attempts to kill all Cursor processes -func (m *Manager) KillCursorProcesses() error { - m.mu.Lock() - defer m.mu.Unlock() - - ctx, cancel := context.WithTimeout(context.Background(), m.config.Timeout) - defer cancel() - - for attempt := 0; attempt < m.config.RetryAttempts; attempt++ { - m.log.Debugf("Attempt %d/%d to kill Cursor processes", attempt+1, m.config.RetryAttempts) - - if err := m.killProcess(ctx); err != nil { - m.log.Warnf("Failed to kill processes on attempt %d: %v", attempt+1, err) - time.Sleep(m.config.RetryDelay) - continue - } - return nil - } - - return fmt.Errorf("failed to kill all Cursor processes after %d attempts", m.config.RetryAttempts) -} - // IsCursorRunning checks if any Cursor process is running func (m *Manager) IsCursorRunning() bool { - m.mu.Lock() - defer m.mu.Unlock() - - processes, err := m.listCursorProcesses() + processes, err := m.getCursorProcesses() if err != nil { - m.log.Warnf("Failed to list Cursor processes: %v", err) + m.log.Warn("Failed to get Cursor processes:", err) return false } - return len(processes) > 0 } -func (m *Manager) killProcess(ctx context.Context) error { - if runtime.GOOS == "windows" { - return m.killWindowsProcess(ctx) - } - return m.killUnixProcess(ctx) -} +// KillCursorProcesses attempts to kill all Cursor processes +func (m *Manager) KillCursorProcesses() error { + for attempt := 1; attempt <= m.config.MaxAttempts; attempt++ { + processes, err := m.getCursorProcesses() + if err != nil { + return fmt.Errorf("failed to get processes: %w", err) + } -func (m *Manager) killWindowsProcess(ctx context.Context) error { - // First try graceful termination - if err := exec.CommandContext(ctx, "taskkill", "/IM", "Cursor.exe").Run(); err != nil { - m.log.Debugf("Graceful termination failed: %v", err) - } + if len(processes) == 0 { + return nil + } - time.Sleep(m.config.RetryDelay) + for _, proc := range processes { + if err := m.killProcess(proc); err != nil { + m.log.Warnf("Failed to kill process %s: %v", proc, err) + } + } + + time.Sleep(m.config.RetryDelay) + } - // Force kill if still running - if err := exec.CommandContext(ctx, "taskkill", "/F", "/IM", "Cursor.exe").Run(); err != nil { - return fmt.Errorf("failed to force kill Cursor process: %w", err) + if m.IsCursorRunning() { + return fmt.Errorf("failed to kill all Cursor processes after %d attempts", m.config.MaxAttempts) } return nil } -func (m *Manager) killUnixProcess(ctx context.Context) error { - processes, err := m.listCursorProcesses() +func (m *Manager) getCursorProcesses() ([]string, error) { + var cmd *exec.Cmd + var processes []string + + switch runtime.GOOS { + case "windows": + cmd = exec.Command("tasklist", "/FO", "CSV", "/NH") + case "darwin": + cmd = exec.Command("ps", "-ax") + case "linux": + cmd = exec.Command("ps", "-A") + default: + return nil, fmt.Errorf("unsupported operating system: %s", runtime.GOOS) + } + + output, err := cmd.Output() if err != nil { - return fmt.Errorf("failed to list processes: %w", err) + return nil, fmt.Errorf("failed to execute command: %w", err) } - for _, pid := range processes { - if err := m.forceKillProcess(ctx, pid); err != nil { - m.log.Warnf("Failed to kill process %s: %v", pid, err) - continue + lines := strings.Split(string(output), "\n") + for _, line := range lines { + for _, pattern := range m.config.ProcessPatterns { + if strings.Contains(strings.ToLower(line), strings.ToLower(pattern)) { + // Extract PID based on OS + pid := m.extractPID(line) + if pid != "" { + processes = append(processes, pid) + } + } } } - return nil + return processes, nil } -func (m *Manager) forceKillProcess(ctx context.Context, pid string) error { - // Try graceful termination first - if err := exec.CommandContext(ctx, "kill", pid).Run(); err == nil { - m.log.Debugf("Process %s terminated gracefully", pid) - time.Sleep(2 * time.Second) - return nil - } - - // Force kill if still running - if err := exec.CommandContext(ctx, "kill", "-9", pid).Run(); err != nil { - return fmt.Errorf("failed to force kill process %s: %w", pid, err) +func (m *Manager) extractPID(line string) string { + switch runtime.GOOS { + case "windows": + // Windows CSV format: "ImageName","PID",... + parts := strings.Split(line, ",") + if len(parts) >= 2 { + return strings.Trim(parts[1], "\"") + } + case "darwin", "linux": + // Unix format: PID TTY TIME CMD + parts := strings.Fields(line) + if len(parts) >= 1 { + return parts[0] + } } - - m.log.Debugf("Process %s force killed", pid) - return nil + return "" } -func (m *Manager) listCursorProcesses() ([]string, error) { - cmd := exec.Command("ps", "aux") - output, err := cmd.Output() - if err != nil { - return nil, fmt.Errorf("failed to execute ps command: %w", err) - } +func (m *Manager) killProcess(pid string) error { + var cmd *exec.Cmd - var pids []string - for _, line := range strings.Split(string(output), "\n") { - if strings.Contains(strings.ToLower(line), "apprun") { - fields := strings.Fields(line) - if len(fields) > 1 { - pids = append(pids, fields[1]) - } - } + switch runtime.GOOS { + case "windows": + cmd = exec.Command("taskkill", "/F", "/PID", pid) + case "darwin", "linux": + cmd = exec.Command("kill", "-9", pid) + default: + return fmt.Errorf("unsupported operating system: %s", runtime.GOOS) } - return pids, nil + return cmd.Run() }