feat: add process management commands for daemon control
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>
This commit is contained in:
		
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -1,4 +1,5 @@
 | 
			
		||||
.claude/
 | 
			
		||||
.kvs/
 | 
			
		||||
data/
 | 
			
		||||
data*/
 | 
			
		||||
*.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/signal"
 | 
			
		||||
	"syscall"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"kvs/config"
 | 
			
		||||
	"kvs/daemon"
 | 
			
		||||
	"kvs/server"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	configPath := "./config.yaml"
 | 
			
		||||
 | 
			
		||||
	// Simple CLI argument parsing
 | 
			
		||||
	if len(os.Args) > 1 {
 | 
			
		||||
		configPath = os.Args[1]
 | 
			
		||||
	if len(os.Args) < 2 {
 | 
			
		||||
		// No arguments - run in foreground with default config
 | 
			
		||||
		runServer("./config.yaml", false)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 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)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "Failed to load configuration: %v\n", err)
 | 
			
		||||
		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)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "Failed to create server: %v\n", err)
 | 
			
		||||
@@ -45,3 +110,135 @@ func main() {
 | 
			
		||||
		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