diff --git a/cmd/chatlog/cmd_server.go b/cmd/chatlog/cmd_server.go index c606f62..9d377fe 100644 --- a/cmd/chatlog/cmd_server.go +++ b/cmd/chatlog/cmd_server.go @@ -36,6 +36,8 @@ var serverCmd = &cobra.Command{ Use: "server", Short: "Start HTTP server", Run: func(cmd *cobra.Command, args []string) { + cleanup := initSingleInstance() + defer cleanup() cmdConf := getServerConfig() log.Info().Msgf("server cmd config: %+v", cmdConf) diff --git a/cmd/chatlog/root.go b/cmd/chatlog/root.go index da88bcb..e45f5ce 100644 --- a/cmd/chatlog/root.go +++ b/cmd/chatlog/root.go @@ -1,7 +1,12 @@ package chatlog import ( + "fmt" + "os" + "github.com/sjzar/chatlog/internal/chatlog" + "github.com/sjzar/chatlog/pkg/process" + "github.com/sjzar/chatlog/pkg/util" "github.com/rs/zerolog/log" "github.com/spf13/cobra" @@ -35,8 +40,20 @@ var rootCmd = &cobra.Command{ } func Root(cmd *cobra.Command, args []string) { + cleanup := initSingleInstance() + defer cleanup() + m := chatlog.New() if err := m.Run(""); err != nil { log.Err(err).Msg("failed to run chatlog instance") } } + +func initSingleInstance() func() { + cleanup, err := process.CheckSingleInstance(util.DefaultWorkDir("")) + if err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + os.Exit(1) + } + return cleanup +} diff --git a/pkg/process/process.go b/pkg/process/process.go new file mode 100644 index 0000000..b04805e --- /dev/null +++ b/pkg/process/process.go @@ -0,0 +1,65 @@ +package process + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + + "github.com/shirou/gopsutil/v4/process" +) + +// CheckSingleInstance checks if another instance is running using a PID file. +// If another instance is found, it prompts the user to force close it. +// Returns a cleanup function to be called on exit. +func CheckSingleInstance(workDir string) (func(), error) { + pidFile := filepath.Join(workDir, "chatlog.pid") + + // Read existing PID file + if content, err := os.ReadFile(pidFile); err == nil { + pidStr := strings.TrimSpace(string(content)) + if pid, err := strconv.Atoi(pidStr); err == nil { + // Check if process exists + if exists, _ := process.PidExists(int32(pid)); exists { + // Process exists, check if it's really us (optional, but good practice) + // For now, just assume if PID exists it might be us or a zombie. + // We can check process name if needed, but pid file is strong hint. + + fmt.Printf("Detected another instance running (PID: %d).\n", pid) + fmt.Print("Do you want to force close it and continue? [y/N]: ") + + reader := bufio.NewReader(os.Stdin) + input, _ := reader.ReadString('\n') + input = strings.TrimSpace(strings.ToLower(input)) + + if input == "y" || input == "yes" { + if p, err := process.NewProcess(int32(pid)); err == nil { + if err := p.Kill(); err != nil { + return nil, fmt.Errorf("failed to kill process: %w", err) + } + fmt.Println("Process killed.") + } else { + // Process might have exited in the meantime + fmt.Println("Process not found, continuing...") + } + } else { + return nil, fmt.Errorf("application already running") + } + } + } + } + + // Write current PID + currentPID := os.Getpid() + if err := os.WriteFile(pidFile, []byte(strconv.Itoa(currentPID)), 0644); err != nil { + return nil, fmt.Errorf("failed to write pid file: %w", err) + } + + // Cleanup function + return func() { + os.Remove(pidFile) + }, + nil +}