forked from ryyst/kalzu-value-store
Add systemd-style subcommands for managing KVS instances: - start <config> - Daemonize and run in background - stop <config> - Gracefully stop daemon - restart <config> - Restart daemon - status [config] - Show status of all or specific instances Key features: - PID files stored in ~/.kvs/pids/ (global across all directories) - Logs stored in ~/.kvs/logs/ - Config names support both 'node1' and 'node1.yaml' formats - Backward compatible: 'kvs config.yaml' still runs in foreground - Proper stale PID detection and cleanup 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
172 lines
3.9 KiB
Go
172 lines
3.9 KiB
Go
package daemon
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
)
|
|
|
|
// getPIDDir returns the absolute path to the PID directory
|
|
func getPIDDir() (string, error) {
|
|
homeDir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get user home directory: %w", err)
|
|
}
|
|
return filepath.Join(homeDir, ".kvs", "pids"), nil
|
|
}
|
|
|
|
// getLogDir returns the absolute path to the log directory
|
|
func getLogDir() (string, error) {
|
|
homeDir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get user home directory: %w", err)
|
|
}
|
|
return filepath.Join(homeDir, ".kvs", "logs"), nil
|
|
}
|
|
|
|
// GetPIDFilePath returns the PID file path for a given config file
|
|
func GetPIDFilePath(configPath string) string {
|
|
pidDir, err := getPIDDir()
|
|
if err != nil {
|
|
// Fallback to local directory
|
|
pidDir = ".kvs/pids"
|
|
}
|
|
|
|
// Extract basename without extension
|
|
basename := filepath.Base(configPath)
|
|
name := strings.TrimSuffix(basename, filepath.Ext(basename))
|
|
|
|
return filepath.Join(pidDir, name+".pid")
|
|
}
|
|
|
|
// EnsurePIDDir creates the PID directory if it doesn't exist
|
|
func EnsurePIDDir() error {
|
|
pidDir, err := getPIDDir()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.MkdirAll(pidDir, 0755)
|
|
}
|
|
|
|
// WritePID writes the current process PID to a file
|
|
func WritePID(configPath string) error {
|
|
if err := EnsurePIDDir(); err != nil {
|
|
return fmt.Errorf("failed to create PID directory: %w", err)
|
|
}
|
|
|
|
pidFile := GetPIDFilePath(configPath)
|
|
pid := os.Getpid()
|
|
|
|
return os.WriteFile(pidFile, []byte(fmt.Sprintf("%d\n", pid)), 0644)
|
|
}
|
|
|
|
// ReadPID reads the PID from a file and checks if the process is running
|
|
func ReadPID(configPath string) (int, bool, error) {
|
|
pidFile := GetPIDFilePath(configPath)
|
|
|
|
data, err := os.ReadFile(pidFile)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return 0, false, nil
|
|
}
|
|
return 0, false, fmt.Errorf("failed to read PID file: %w", err)
|
|
}
|
|
|
|
pidStr := strings.TrimSpace(string(data))
|
|
pid, err := strconv.Atoi(pidStr)
|
|
if err != nil {
|
|
return 0, false, fmt.Errorf("invalid PID in file: %w", err)
|
|
}
|
|
|
|
// Check if process is actually running
|
|
process, err := os.FindProcess(pid)
|
|
if err != nil {
|
|
return pid, false, nil
|
|
}
|
|
|
|
// Send signal 0 to check if process exists
|
|
err = process.Signal(syscall.Signal(0))
|
|
if err != nil {
|
|
return pid, false, nil
|
|
}
|
|
|
|
return pid, true, nil
|
|
}
|
|
|
|
// RemovePID removes the PID file
|
|
func RemovePID(configPath string) error {
|
|
pidFile := GetPIDFilePath(configPath)
|
|
err := os.Remove(pidFile)
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return fmt.Errorf("failed to remove PID file: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ListRunningInstances returns a list of running KVS instances
|
|
func ListRunningInstances() ([]InstanceInfo, error) {
|
|
var instances []InstanceInfo
|
|
|
|
pidDir, err := getPIDDir()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Check if PID directory exists
|
|
if _, err := os.Stat(pidDir); os.IsNotExist(err) {
|
|
return instances, nil
|
|
}
|
|
|
|
entries, err := os.ReadDir(pidDir)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read PID directory: %w", err)
|
|
}
|
|
|
|
for _, entry := range entries {
|
|
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".pid") {
|
|
continue
|
|
}
|
|
|
|
name := strings.TrimSuffix(entry.Name(), ".pid")
|
|
configPath := name + ".yaml" // Assume .yaml extension
|
|
|
|
pid, running, err := ReadPID(configPath)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
instances = append(instances, InstanceInfo{
|
|
Name: name,
|
|
PID: pid,
|
|
Running: running,
|
|
})
|
|
}
|
|
|
|
return instances, nil
|
|
}
|
|
|
|
// InstanceInfo holds information about a KVS instance
|
|
type InstanceInfo struct {
|
|
Name string
|
|
PID int
|
|
Running bool
|
|
}
|
|
|
|
// StopProcess stops a process by PID
|
|
func StopProcess(pid int) error {
|
|
process, err := os.FindProcess(pid)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to find process: %w", err)
|
|
}
|
|
|
|
// Try graceful shutdown first (SIGTERM)
|
|
if err := process.Signal(syscall.SIGTERM); err != nil {
|
|
return fmt.Errorf("failed to send SIGTERM: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|