Browse Source

feat: improve process management for auto-closing Cursor instances

pull/85/head
dacrab 5 months ago
parent
commit
56f09677ca
  1. 176
      internal/process/manager.go

176
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()
}
Loading…
Cancel
Save