package main import ( "errors" "fmt" "io/fs" "log" "os" "os/user" "path/filepath" "strconv" "strings" processInfo "github.com/shirou/gopsutil/process" ) func checkAndCreateInstanceLock(path, name string, perUser bool) (pid int32, err error) { lockFilePath := getLockFilePath(path, name, perUser) // read current pid file data, err := os.ReadFile(lockFilePath) if err != nil { if errors.Is(err, fs.ErrNotExist) { // create new lock return 0, createInstanceLock(lockFilePath) } return 0, err } // file exists! parsedPid, err := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64) if err != nil { log.Printf("failed to parse existing lock pid file (ignoring): %s\n", err) return 0, createInstanceLock(lockFilePath) } // Check if process exists. p, err := processInfo.NewProcess(int32(parsedPid)) switch { case err == nil: // Process exists, continue. case errors.Is(err, processInfo.ErrorProcessNotRunning): // A process with the locked PID does not exist. // This is expected, so we can continue normally. return 0, createInstanceLock(lockFilePath) default: // There was an internal error getting the process. return 0, err } // Get the process paths and evaluate and clean them. executingBinaryPath, err := p.Exe() if err != nil { return 0, fmt.Errorf("failed to get path of existing process: %w", err) } cleanedExecutingBinaryPath, err := filepath.EvalSymlinks(executingBinaryPath) if err != nil { return 0, fmt.Errorf("failed to evaluate path of existing process: %w", err) } // Check if the binary is portmaster-start with high probability. if !strings.Contains(filepath.Base(cleanedExecutingBinaryPath), "portmaster-start") { // The process with the locked PID belongs to another binary. // As the Portmaster usually starts very early, it will have a low PID, // which could be assigned to another process on next boot. return 0, createInstanceLock(lockFilePath) } // Return PID of already running instance. return p.Pid, nil } func createInstanceLock(lockFilePath string) error { // check data root dir err := dataRoot.Ensure() if err != nil { log.Printf("failed to check data root dir: %s\n", err) } // create lock file // TODO: Investigate required permissions. err = os.WriteFile(lockFilePath, []byte(fmt.Sprintf("%d", os.Getpid())), 0o0666) //nolint:gosec if err != nil { return err } return nil } func deleteInstanceLock(path, name string, perUser bool) error { return os.Remove(getLockFilePath(path, name, perUser)) } func getLockFilePath(path, name string, perUser bool) string { if !perUser { return filepath.Join(dataRoot.Path, path, fmt.Sprintf("%s-lock.pid", name)) } // Get user ID for per-user lock file. var userID string usr, err := user.Current() if err != nil { log.Printf("failed to get current user: %s\n", err) userID = "no-user" } else { userID = usr.Uid } return filepath.Join(dataRoot.Path, path, fmt.Sprintf("%s-%s-lock.pid", name, userID)) }