212 lines
5.0 KiB
Go
212 lines
5.0 KiB
Go
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)
|
|
}
|
|
}
|