mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
163 lines
4.2 KiB
Go
163 lines
4.2 KiB
Go
package pid
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/sipeed/picoclaw/pkg/config"
|
|
"github.com/sipeed/picoclaw/pkg/logger"
|
|
)
|
|
|
|
const pidFileName = ".picoclaw.pid"
|
|
|
|
// PidFileData is the JSON structure stored in the PID file.
|
|
type PidFileData struct {
|
|
PID int `json:"pid"`
|
|
Token string `json:"token"`
|
|
Version string `json:"version"`
|
|
Port int `json:"port"`
|
|
Host string `json:"host"`
|
|
}
|
|
|
|
var pidMu sync.Mutex
|
|
|
|
// pidFilePath returns the absolute path for the PID file given the home directory.
|
|
func pidFilePath(homePath string) string {
|
|
return filepath.Join(homePath, pidFileName)
|
|
}
|
|
|
|
// generateToken creates a cryptographically random 32-character hex token.
|
|
func generateToken() string {
|
|
b := make([]byte, 16)
|
|
if _, err := rand.Read(b); err != nil {
|
|
// Fallback to something pseudo-random if crypto/rand fails
|
|
return fmt.Sprintf("%032x", time.Now().UnixNano())
|
|
}
|
|
return hex.EncodeToString(b)
|
|
}
|
|
|
|
// WritePidFile creates (or overwrites) the PID file atomically.
|
|
// It returns an error if another gateway instance appears to be running
|
|
// (a valid PID file exists with a live process).
|
|
func WritePidFile(homePath, host string, port int) (*PidFileData, error) {
|
|
pidMu.Lock()
|
|
defer pidMu.Unlock()
|
|
|
|
pidPath := pidFilePath(homePath)
|
|
|
|
// Check for existing PID file → singleton enforcement.
|
|
if data, err := readPidFileUnlocked(pidPath); err == nil {
|
|
if os.Getpid() != data.PID {
|
|
logger.Infof("found pid file (PID: %d, version: %s)", data.PID, data.Version)
|
|
if isProcessRunning(data.PID) {
|
|
return nil, fmt.Errorf("gateway is already running (PID: %d, version: %s)", data.PID, data.Version)
|
|
}
|
|
logger.Warnf("not running (PID: %d) so will remove the pid file: %s", data.PID, pidPath)
|
|
}
|
|
// Stale PID file; process no longer exists → clean up.
|
|
os.Remove(pidPath)
|
|
}
|
|
|
|
data := &PidFileData{
|
|
PID: os.Getpid(),
|
|
Version: config.GetVersion(),
|
|
Port: port,
|
|
Host: host,
|
|
}
|
|
|
|
token := generateToken()
|
|
data.Token = token
|
|
|
|
raw, err := json.MarshalIndent(data, "", " ")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal pid file: %w", err)
|
|
}
|
|
|
|
// Ensure parent directory exists.
|
|
dir := filepath.Dir(pidPath)
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
return nil, fmt.Errorf("failed to create pid directory: %w", err)
|
|
}
|
|
|
|
// Write atomically via temp file + rename.
|
|
tmp := pidPath + ".tmp"
|
|
if err := os.WriteFile(tmp, raw, 0o600); err != nil {
|
|
return nil, fmt.Errorf("failed to write pid file: %w", err)
|
|
}
|
|
if err := os.Rename(tmp, pidPath); err != nil {
|
|
os.Remove(tmp)
|
|
return nil, fmt.Errorf("failed to rename pid file: %w", err)
|
|
}
|
|
logger.Debugf("wrote pid file: %s success", pidPath)
|
|
|
|
return data, nil
|
|
}
|
|
|
|
// ReadPidFileWithCheck reads the PID file and additionally checks if
|
|
// the recorded process is still alive. Returns nil if the file is
|
|
// missing, unreadable, or the process has exited.
|
|
func ReadPidFileWithCheck(homePath string) *PidFileData {
|
|
pidMu.Lock()
|
|
defer pidMu.Unlock()
|
|
|
|
pidPath := pidFilePath(homePath)
|
|
data, err := readPidFileUnlocked(pidPath)
|
|
if err != nil {
|
|
logger.Debugf("failed to read pid file: %s", err)
|
|
return nil
|
|
}
|
|
|
|
if !isProcessRunning(data.PID) {
|
|
logger.Debugf("process not running, remove pid file: %s", pidPath)
|
|
os.Remove(pidPath)
|
|
return nil
|
|
}
|
|
|
|
return data
|
|
}
|
|
|
|
// RemovePidFile deletes the PID file (e.g. on graceful shutdown).
|
|
func RemovePidFile(homePath string) {
|
|
pidMu.Lock()
|
|
defer pidMu.Unlock()
|
|
|
|
pidPath := pidFilePath(homePath)
|
|
// Only remove if the PID matches our own process (avoid deleting
|
|
// a file that belongs to a newer gateway instance).
|
|
if data, err := readPidFileUnlocked(pidPath); err == nil {
|
|
if data.PID != os.Getpid() {
|
|
return
|
|
}
|
|
}
|
|
|
|
logger.Infof("remove pid file: %s", pidPath)
|
|
os.Remove(pidPath)
|
|
}
|
|
|
|
// readPidFileUnlocked reads the PID file without acquiring the lock.
|
|
// Caller must hold pidMu.
|
|
func readPidFileUnlocked(pidPath string) (*PidFileData, error) {
|
|
raw, err := os.ReadFile(pidPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var data PidFileData
|
|
if err := json.Unmarshal(raw, &data); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Validate PID is a positive integer.
|
|
if data.PID <= 0 {
|
|
return nil, fmt.Errorf("invalid pid in pid file: %d", data.PID)
|
|
}
|
|
|
|
return &data, nil
|
|
}
|