From 72705afc16a3173c32b64b2bfbaa831ccc573b58 Mon Sep 17 00:00:00 2001 From: Menno van Leeuwen Date: Wed, 21 May 2025 00:07:48 +0200 Subject: [PATCH] feat: add stop command to terminate the clipboard watcher daemon --- src/commands/cmd_status.go | 24 +++++++++++++---- src/commands/cmd_stop.go | 54 ++++++++++++++++++++++++++++++++++++++ src/commands/cmd_watch.go | 30 ++++++++++++++++----- src/config/config.go | 6 ++++- src/main.go | 1 + 5 files changed, 102 insertions(+), 13 deletions(-) create mode 100644 src/commands/cmd_stop.go diff --git a/src/commands/cmd_status.go b/src/commands/cmd_status.go index ac89376..505c8c7 100644 --- a/src/commands/cmd_status.go +++ b/src/commands/cmd_status.go @@ -1,6 +1,8 @@ package commands import ( + "fmt" + "os" "os/exec" "strings" @@ -14,14 +16,26 @@ func NewStatusCmd() *cobra.Command { Use: "status", Short: "Check if the clipboard watcher daemon is running", Run: func(cmd *cobra.Command, args []string) { - // Check for the process by name (simple approach) - out, err := exec.Command("ps", "-C", "kcm").Output() + out, err := exec.Command("ps", "-C", "kcm", "-o", "pid=").Output() if err != nil { log.Fatal().Err(err).Msg("Failed to check kcm daemon status") } - lines := strings.Split(string(out), "\n") - if len(lines) > 1 { - log.Info().Msg("kcm daemon is running.") + pids := strings.Fields(string(out)) + currentPID := os.Getpid() + filteredPIDs := make([]string, 0, len(pids)) + for _, pidStr := range pids { + if pidStr == "" { + continue + } + if pidStr == fmt.Sprintf("%d", currentPID) { + continue + } + filteredPIDs = append(filteredPIDs, pidStr) + } + if len(filteredPIDs) > 0 { + log.Info(). + Strs("pids", filteredPIDs). + Msg("kcm daemon is running.") } else { log.Warn().Msg("kcm daemon is not running, start a new one using `kcm watch`.") } diff --git a/src/commands/cmd_stop.go b/src/commands/cmd_stop.go new file mode 100644 index 0000000..9620a27 --- /dev/null +++ b/src/commands/cmd_stop.go @@ -0,0 +1,54 @@ +package commands + +import ( + "os" + "os/exec" + "strconv" + "strings" + + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" +) + +// NewStopCmd creates a new stop command to kill the kcm daemon. +func NewStopCmd() *cobra.Command { + return &cobra.Command{ + Use: "stop", + Short: "Stop the clipboard watcher daemon", + Aliases: []string{"--stop", "kill"}, + Run: func(cmd *cobra.Command, args []string) { + out, err := exec.Command("ps", "-C", "kcm", "-o", "pid=").Output() + if err != nil { + log.Fatal().Err(err).Msg("Failed to check for running kcm processes") + } + pids := strings.Fields(string(out)) + if len(pids) == 0 { + log.Info().Msg("No running kcm daemon found.") + return + } + killed := 0 + for _, pidStr := range pids { + pid, err := strconv.Atoi(pidStr) + if err != nil { + continue + } + if pid == os.Getpid() { + continue // Don't kill self + } + proc, err := os.FindProcess(pid) + if err == nil { + err = proc.Kill() + if err == nil { + log.Info().Msgf("Killed kcm daemon with PID %d", pid) + killed++ + } + } + } + if killed == 0 { + log.Warn().Msg("No kcm daemons were killed.") + } else { + log.Info().Msgf("Stopped %d kcm daemon(s).", killed) + } + }, + } +} diff --git a/src/commands/cmd_watch.go b/src/commands/cmd_watch.go index e75bf44..efa4972 100644 --- a/src/commands/cmd_watch.go +++ b/src/commands/cmd_watch.go @@ -2,6 +2,9 @@ package commands import ( "os" + "os/exec" + "strconv" + "strings" "time" "github.com/rs/zerolog" @@ -20,9 +23,22 @@ func NewWatchCmd(history *models.History) *cobra.Command { Run: func(cmd *cobra.Command, args []string) { noDaemon, _ := cmd.Flags().GetBool("no-daemon") if !noDaemon { - cfg, _ := config.LoadConfig() + // Safety net: check if kcm daemon is already running (ignore self) + out, err := exec.Command("ps", "-C", "kcm", "-o", "pid=").Output() + if err == nil { + pids := strings.Fields(string(out)) + myPid := os.Getpid() + for _, pidStr := range pids { + pid, err := strconv.Atoi(pidStr) + if err == nil && pid != myPid { + log.Warn().Msg("kcm daemon is already running. Exiting to avoid duplicate daemons.") + os.Exit(1) + } + } + } // Set log file path since we are daemonizing + cfg, _ := config.LoadConfig() if cfg.Logging.Path != "" { file, err := os.OpenFile(cfg.Logging.Path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) if err != nil { @@ -46,13 +62,13 @@ func NewWatchCmd(history *models.History) *cobra.Command { if err != nil { log.Fatal().Err(err).Msg("Failed to daemonize") } - log.Info().Msgf("Daemon started with PID %d", proc.Pid) + log.Info(). + Str("logging_path", cfg.Logging.Path). + Msgf("Daemon started with PID %d", proc.Pid) os.Exit(0) } } - log.Info().Msg("Starting clipboard watcher (golang.design/x/clipboard)...") - if err := clipboard.Init(); err != nil { log.Fatal().Err(err).Msg("Failed to initialize clipboard") } @@ -63,7 +79,7 @@ func NewWatchCmd(history *models.History) *cobra.Command { ) for { - time.Sleep(500 * time.Millisecond) + time.Sleep(250 * time.Millisecond) textOut := clipboard.Read(clipboard.FmtText) imgOut := clipboard.Read(clipboard.FmtImage) @@ -79,7 +95,7 @@ func NewWatchCmd(history *models.History) *cobra.Command { } history.Add(item) lastText = text - log.Info().Str("content", text).Msg("Text clipboard item added to history") + log.Info().Str("content", text).Msg("Text item added ") } } @@ -92,7 +108,7 @@ func NewWatchCmd(history *models.History) *cobra.Command { } history.Add(item) lastImage = imgOut - log.Info().Msg("Image clipboard item added to history") + log.Info().Msg("Image item added") } } }, diff --git a/src/config/config.go b/src/config/config.go index 7619a2b..fdffd90 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -37,11 +37,15 @@ func LoadConfig() (Config, string) { if err != nil { continue } - defer file.Close() d := yaml.NewDecoder(file) if err := d.Decode(&cfg); err == nil { + file.Close() + if cfg.Logging.Path == "" { + cfg.Logging.Path = logFilePath() + } return cfg, path } + file.Close() } cfg.Logging.Format = "console" diff --git a/src/main.go b/src/main.go index 4796414..a04e050 100644 --- a/src/main.go +++ b/src/main.go @@ -56,6 +56,7 @@ func main() { commands.NewSearchCmd(history), commands.NewStatusCmd(), commands.NewVersionCmd(), + commands.NewStopCmd(), // Add the stop command ) if err := rootCmd.Execute(); err != nil {