package cmd import ( "fmt" "os" "os/exec" "path/filepath" "strconv" "syscall" "github.com/spf13/cobra" "github.com/yeho/klp/internal/database" "github.com/yeho/klp/internal/service" ) var ( foregroundFlag bool ) var serviceCmd = &cobra.Command{ Use: "service", Short: "Service management commands", Long: `Commands for managing the clipboard monitoring service.`, } var serviceStartCmd = &cobra.Command{ Use: "start", Short: "Start the clipboard monitor", Long: `Start the clipboard monitoring service as a background daemon. The service monitors clipboard changes and stores them in the database. Use --foreground to run in foreground instead of daemonizing. Use 'klp service stop' to stop the daemon.`, Run: func(cmd *cobra.Command, args []string) { if !foregroundFlag && os.Getenv("KLP_DAEMON") != "1" { // Daemonize: re-exec ourselves in background daemonize() return } // Running as daemon or in foreground mode runMonitor() }, } var serviceStopCmd = &cobra.Command{ Use: "stop", Short: "Stop the clipboard monitor daemon", Run: func(cmd *cobra.Command, args []string) { pidFile := getPidFile() data, err := os.ReadFile(pidFile) if err != nil { fmt.Println("No running daemon found") return } pid, err := strconv.Atoi(string(data)) if err != nil { fmt.Fprintf(os.Stderr, "Invalid PID file: %v\n", err) os.Exit(1) } process, err := os.FindProcess(pid) if err != nil { fmt.Println("No running daemon found") os.Remove(pidFile) return } if err := process.Signal(syscall.SIGTERM); err != nil { fmt.Printf("Daemon not running (PID %d)\n", pid) os.Remove(pidFile) return } fmt.Printf("Stopped daemon (PID %d)\n", pid) os.Remove(pidFile) }, } var serviceStatusCmd = &cobra.Command{ Use: "status", Short: "Check if the clipboard monitor is running", Run: func(cmd *cobra.Command, args []string) { pidFile := getPidFile() data, err := os.ReadFile(pidFile) if err != nil { fmt.Println("Daemon is not running") return } pid, err := strconv.Atoi(string(data)) if err != nil { fmt.Println("Daemon is not running (invalid PID file)") return } // Check if process exists process, err := os.FindProcess(pid) if err != nil { fmt.Println("Daemon is not running") os.Remove(pidFile) return } // On Unix, FindProcess always succeeds, so we need to send signal 0 to check if err := process.Signal(syscall.Signal(0)); err != nil { fmt.Println("Daemon is not running") os.Remove(pidFile) return } fmt.Printf("Daemon is running (PID %d)\n", pid) }, } func init() { rootCmd.AddCommand(serviceCmd) serviceCmd.AddCommand(serviceStartCmd) serviceCmd.AddCommand(serviceStopCmd) serviceCmd.AddCommand(serviceStatusCmd) serviceStartCmd.Flags().BoolVarP(&foregroundFlag, "foreground", "f", false, "Run in foreground instead of daemonizing") } func getPidFile() string { homeDir, _ := os.UserHomeDir() return filepath.Join(homeDir, ".config", "klp", "klp.pid") } func getLogFile() string { homeDir, _ := os.UserHomeDir() return filepath.Join(homeDir, ".config", "klp", "klp.log") } func daemonize() { // Check if already running pidFile := getPidFile() if data, err := os.ReadFile(pidFile); err == nil { if pid, err := strconv.Atoi(string(data)); err == nil { if process, err := os.FindProcess(pid); err == nil { if err := process.Signal(syscall.Signal(0)); err == nil { fmt.Printf("Daemon already running (PID %d)\n", pid) return } } } } // Get executable path executable, err := os.Executable() if err != nil { fmt.Fprintf(os.Stderr, "Error getting executable path: %v\n", err) os.Exit(1) } // Ensure config directory exists os.MkdirAll(filepath.Dir(pidFile), 0755) // Open log file logFile, err := os.OpenFile(getLogFile(), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) if err != nil { fmt.Fprintf(os.Stderr, "Error opening log file: %v\n", err) os.Exit(1) } // Start daemon process cmd := exec.Command(executable, "service", "start", "--foreground") cmd.Env = append(os.Environ(), "KLP_DAEMON=1") cmd.Stdout = logFile cmd.Stderr = logFile cmd.SysProcAttr = &syscall.SysProcAttr{ Setsid: true, // Create new session } if err := cmd.Start(); err != nil { fmt.Fprintf(os.Stderr, "Error starting daemon: %v\n", err) os.Exit(1) } // Write PID file if err := os.WriteFile(pidFile, []byte(strconv.Itoa(cmd.Process.Pid)), 0644); err != nil { fmt.Fprintf(os.Stderr, "Error writing PID file: %v\n", err) } fmt.Printf("Daemon started (PID %d)\n", cmd.Process.Pid) fmt.Printf("Log file: %s\n", getLogFile()) } func runMonitor() { db, err := database.Load(cfg.DBPath()) if err != nil { fmt.Fprintf(os.Stderr, "Error loading database: %v\n", err) os.Exit(1) } monitor, err := service.NewMonitor(cfg, db) if err != nil { fmt.Fprintf(os.Stderr, "Error creating monitor: %v\n", err) os.Exit(1) } // Clean up PID file on exit pidFile := getPidFile() defer os.Remove(pidFile) if err := monitor.Run(); err != nil { fmt.Fprintf(os.Stderr, "Error running monitor: %v\n", err) os.Exit(1) } }