self-daemonize #16
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,4 +1,5 @@
|
|||||||
.claude/
|
.claude/
|
||||||
|
.kvs/
|
||||||
data/
|
data/
|
||||||
data*/
|
data*/
|
||||||
*.yaml
|
*.yaml
|
||||||
|
87
daemon/daemonize.go
Normal file
87
daemon/daemonize.go
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
package daemon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetLogFilePath returns the log file path for a given config file
|
||||||
|
func GetLogFilePath(configPath string) (string, error) {
|
||||||
|
logDir, err := getLogDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
absConfigPath, err := filepath.Abs(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get absolute config path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
basename := filepath.Base(configPath)
|
||||||
|
name := filepath.Base(filepath.Dir(absConfigPath)) + "_" + basename
|
||||||
|
return filepath.Join(logDir, name+".log"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Daemonize spawns the process as a daemon and returns
|
||||||
|
func Daemonize(configPath string) error {
|
||||||
|
// Get absolute path to the current executable
|
||||||
|
executable, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get executable path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get absolute path to config
|
||||||
|
absConfigPath, err := filepath.Abs(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get absolute config path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if already running
|
||||||
|
_, running, err := ReadPID(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check if instance is running: %w", err)
|
||||||
|
}
|
||||||
|
if running {
|
||||||
|
return fmt.Errorf("instance is already running")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spawn the process in background with --daemon flag
|
||||||
|
cmd := exec.Command(executable, "--daemon", absConfigPath)
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
|
Setsid: true, // Create new session
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect stdout/stderr to log file
|
||||||
|
logDir, err := getLogDir()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get log directory: %w", err)
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(logDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create log directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
basename := filepath.Base(configPath)
|
||||||
|
name := filepath.Base(filepath.Dir(absConfigPath)) + "_" + basename
|
||||||
|
logFile := filepath.Join(logDir, name+".log")
|
||||||
|
|
||||||
|
f, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open log file: %w", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
cmd.Stdout = f
|
||||||
|
cmd.Stderr = f
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return fmt.Errorf("failed to start daemon: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Started KVS instance '%s' (PID will be written by daemon)\n", filepath.Base(configPath))
|
||||||
|
fmt.Printf("Logs: %s\n", logFile)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
171
daemon/pid.go
Normal file
171
daemon/pid.go
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
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
|
||||||
|
}
|
207
main.go
207
main.go
@@ -6,25 +6,90 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"kvs/config"
|
"kvs/config"
|
||||||
|
"kvs/daemon"
|
||||||
"kvs/server"
|
"kvs/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
configPath := "./config.yaml"
|
if len(os.Args) < 2 {
|
||||||
|
// No arguments - run in foreground with default config
|
||||||
// Simple CLI argument parsing
|
runServer("./config.yaml", false)
|
||||||
if len(os.Args) > 1 {
|
return
|
||||||
configPath = os.Args[1]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if this is a daemon spawn
|
||||||
|
if os.Args[1] == "--daemon" {
|
||||||
|
if len(os.Args) < 3 {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: --daemon flag requires config path\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
runServer(os.Args[2], true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse subcommand
|
||||||
|
command := os.Args[1]
|
||||||
|
|
||||||
|
switch command {
|
||||||
|
case "start":
|
||||||
|
if len(os.Args) < 3 {
|
||||||
|
fmt.Fprintf(os.Stderr, "Usage: kvs start <config>\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
cmdStart(normalizeConfigPath(os.Args[2]))
|
||||||
|
|
||||||
|
case "stop":
|
||||||
|
if len(os.Args) < 3 {
|
||||||
|
fmt.Fprintf(os.Stderr, "Usage: kvs stop <config>\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
cmdStop(normalizeConfigPath(os.Args[2]))
|
||||||
|
|
||||||
|
case "restart":
|
||||||
|
if len(os.Args) < 3 {
|
||||||
|
fmt.Fprintf(os.Stderr, "Usage: kvs restart <config>\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
cmdRestart(normalizeConfigPath(os.Args[2]))
|
||||||
|
|
||||||
|
case "status":
|
||||||
|
if len(os.Args) > 2 {
|
||||||
|
cmdStatusSingle(normalizeConfigPath(os.Args[2]))
|
||||||
|
} else {
|
||||||
|
cmdStatusAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
case "help", "--help", "-h":
|
||||||
|
printHelp()
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Backward compatibility: assume it's a config file path
|
||||||
|
runServer(command, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runServer(configPath string, isDaemon bool) {
|
||||||
cfg, err := config.Load(configPath)
|
cfg, err := config.Load(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Failed to load configuration: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Failed to load configuration: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Write PID file if running as daemon
|
||||||
|
if isDaemon {
|
||||||
|
if err := daemon.WritePID(configPath); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to write PID file: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer daemon.RemovePID(configPath)
|
||||||
|
}
|
||||||
|
|
||||||
kvServer, err := server.NewServer(cfg)
|
kvServer, err := server.NewServer(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Failed to create server: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Failed to create server: %v\n", err)
|
||||||
@@ -45,3 +110,135 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cmdStart(configPath string) {
|
||||||
|
if err := daemon.Daemonize(configPath); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to start: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdStop(configPath string) {
|
||||||
|
pid, running, err := daemon.ReadPID(configPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to read PID: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !running {
|
||||||
|
fmt.Printf("Instance '%s' is not running\n", configPath)
|
||||||
|
// Clean up stale PID file
|
||||||
|
daemon.RemovePID(configPath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Stopping instance '%s' (PID %d)...\n", configPath, pid)
|
||||||
|
if err := daemon.StopProcess(pid); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to stop process: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait a bit and verify it stopped
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
_, stillRunning, _ := daemon.ReadPID(configPath)
|
||||||
|
if stillRunning {
|
||||||
|
fmt.Printf("Warning: Process may still be running\n")
|
||||||
|
} else {
|
||||||
|
daemon.RemovePID(configPath)
|
||||||
|
fmt.Printf("Stopped successfully\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdRestart(configPath string) {
|
||||||
|
// Check if running
|
||||||
|
_, running, err := daemon.ReadPID(configPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to check status: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if running {
|
||||||
|
cmdStop(configPath)
|
||||||
|
// Wait a bit for clean shutdown
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdStart(configPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdStatusSingle(configPath string) {
|
||||||
|
pid, running, err := daemon.ReadPID(configPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to read PID: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if running {
|
||||||
|
fmt.Printf("Instance '%s': RUNNING (PID %d)\n", configPath, pid)
|
||||||
|
} else if pid > 0 {
|
||||||
|
fmt.Printf("Instance '%s': STOPPED (stale PID %d)\n", configPath, pid)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Instance '%s': STOPPED\n", configPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdStatusAll() {
|
||||||
|
instances, err := daemon.ListRunningInstances()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to list instances: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(instances) == 0 {
|
||||||
|
fmt.Println("No KVS instances found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("KVS Instances:")
|
||||||
|
for _, inst := range instances {
|
||||||
|
status := "STOPPED"
|
||||||
|
if inst.Running {
|
||||||
|
status = "RUNNING"
|
||||||
|
}
|
||||||
|
fmt.Printf(" %-20s %s (PID %d)\n", inst.Name, status, inst.PID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalizeConfigPath ensures config path has .yaml extension if not specified
|
||||||
|
func normalizeConfigPath(path string) string {
|
||||||
|
// If path doesn't have an extension, add .yaml
|
||||||
|
if filepath.Ext(path) == "" {
|
||||||
|
return path + ".yaml"
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
// getConfigIdentifier returns the identifier for a config (basename without extension)
|
||||||
|
// This is used for PID files and status display
|
||||||
|
func getConfigIdentifier(path string) string {
|
||||||
|
basename := filepath.Base(path)
|
||||||
|
return strings.TrimSuffix(basename, filepath.Ext(basename))
|
||||||
|
}
|
||||||
|
|
||||||
|
func printHelp() {
|
||||||
|
help := `KVS - Distributed Key-Value Store
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
kvs [config.yaml] Run in foreground (default: ./config.yaml)
|
||||||
|
kvs start <config> Start as daemon (.yaml extension optional)
|
||||||
|
kvs stop <config> Stop daemon (.yaml extension optional)
|
||||||
|
kvs restart <config> Restart daemon (.yaml extension optional)
|
||||||
|
kvs status [config] Show status (all instances if no config given)
|
||||||
|
kvs help Show this help
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
kvs # Run with ./config.yaml in foreground
|
||||||
|
kvs node1.yaml # Run with node1.yaml in foreground
|
||||||
|
kvs start node1 # Start node1.yaml as daemon
|
||||||
|
kvs start node1.yaml # Same as above
|
||||||
|
kvs stop node1 # Stop node1 daemon
|
||||||
|
kvs status # Show all running instances
|
||||||
|
kvs status node1 # Show status of node1
|
||||||
|
`
|
||||||
|
fmt.Print(help)
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user