feat: add application mutex to prevent multiple instances

This commit is contained in:
lx1056758714-glitch
2025-12-26 14:56:16 +08:00
parent f63e82ca50
commit 8081bf78e9
3 changed files with 84 additions and 0 deletions

View File

@@ -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)

View File

@@ -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
}

65
pkg/process/process.go Normal file
View File

@@ -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
}