Compare commits
2 Commits
377af163f0
...
4c3fcbc45a
Author | SHA1 | Date | |
---|---|---|---|
4c3fcbc45a | |||
a41e0d625c |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,6 +1,8 @@
|
||||
.claude/
|
||||
.kvs/
|
||||
data/
|
||||
data*/
|
||||
integration_test/
|
||||
*.yaml
|
||||
!config.yaml
|
||||
kvs
|
||||
|
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
|
||||
}
|
@@ -45,6 +45,7 @@ cleanup() {
|
||||
log_info "Cleaning up test environment..."
|
||||
pkill -f "$BINARY" 2>/dev/null || true
|
||||
rm -rf "$TEST_DIR" 2>/dev/null || true
|
||||
rm -rf "$HOME/.kvs" 2>/dev/null || true # Clean up PID and log files from home dir
|
||||
sleep 2 # Allow processes to fully terminate
|
||||
}
|
||||
|
||||
@@ -53,7 +54,7 @@ wait_for_service() {
|
||||
local port=$1
|
||||
local timeout=${2:-30}
|
||||
local count=0
|
||||
|
||||
|
||||
while [ $count -lt $timeout ]; do
|
||||
if curl -s "http://localhost:$port/health" >/dev/null 2>&1; then
|
||||
return 0
|
||||
@@ -64,6 +65,15 @@ wait_for_service() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# Get log file path for a config file (matches daemon naming convention)
|
||||
get_log_file() {
|
||||
local config=$1
|
||||
local abs_path=$(realpath "$config")
|
||||
local basename=$(basename "$config")
|
||||
local dirname=$(basename $(dirname "$abs_path"))
|
||||
echo "$HOME/.kvs/logs/${dirname}_${basename}.log"
|
||||
}
|
||||
|
||||
# Test 1: Build verification
|
||||
test_build() {
|
||||
test_start "Binary build verification"
|
||||
@@ -82,7 +92,7 @@ test_build() {
|
||||
# Test 2: Basic functionality
|
||||
test_basic_functionality() {
|
||||
test_start "Basic functionality test"
|
||||
|
||||
|
||||
# Create basic config
|
||||
cat > basic.yaml <<EOF
|
||||
node_id: "basic-test"
|
||||
@@ -94,20 +104,20 @@ log_level: "error"
|
||||
allow_anonymous_read: true
|
||||
allow_anonymous_write: true
|
||||
EOF
|
||||
|
||||
# Start node
|
||||
$BINARY basic.yaml >/dev/null 2>&1 &
|
||||
local pid=$!
|
||||
|
||||
|
||||
# Start node using daemon command
|
||||
$BINARY start basic.yaml >/dev/null 2>&1
|
||||
sleep 2
|
||||
|
||||
if wait_for_service 8090; then
|
||||
# Test basic CRUD
|
||||
local put_result=$(curl -s -X PUT http://localhost:8090/kv/test/basic \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"message":"hello world"}')
|
||||
|
||||
|
||||
local get_result=$(curl -s http://localhost:8090/kv/test/basic)
|
||||
local message=$(echo "$get_result" | jq -r '.data.message' 2>/dev/null) # Adjusted jq path
|
||||
|
||||
local message=$(echo "$get_result" | jq -r '.data.message' 2>/dev/null)
|
||||
|
||||
if [ "$message" = "hello world" ]; then
|
||||
log_success "Basic CRUD operations work"
|
||||
else
|
||||
@@ -116,9 +126,9 @@ EOF
|
||||
else
|
||||
log_error "Basic test node failed to start"
|
||||
fi
|
||||
|
||||
kill $pid 2>/dev/null || true
|
||||
sleep 2
|
||||
|
||||
$BINARY stop basic.yaml >/dev/null 2>&1
|
||||
sleep 1
|
||||
}
|
||||
|
||||
# Test 3: Cluster formation
|
||||
@@ -160,23 +170,22 @@ allow_anonymous_write: true
|
||||
cluster_secret: "$CLUSTER_SECRET"
|
||||
EOF
|
||||
|
||||
# Start nodes
|
||||
$BINARY cluster1.yaml >/dev/null 2>&1 &
|
||||
local pid1=$!
|
||||
|
||||
# Start nodes using daemon commands
|
||||
$BINARY start cluster1.yaml >/dev/null 2>&1
|
||||
sleep 2
|
||||
|
||||
if ! wait_for_service 8101; then
|
||||
log_error "Cluster node 1 failed to start"
|
||||
kill $pid1 2>/dev/null || true
|
||||
$BINARY stop cluster1.yaml >/dev/null 2>&1
|
||||
return 1
|
||||
fi
|
||||
|
||||
sleep 2 # Give node 1 a moment to fully initialize
|
||||
$BINARY cluster2.yaml >/dev/null 2>&1 &
|
||||
local pid2=$!
|
||||
|
||||
|
||||
$BINARY start cluster2.yaml >/dev/null 2>&1
|
||||
sleep 2
|
||||
|
||||
if ! wait_for_service 8102; then
|
||||
log_error "Cluster node 2 failed to start"
|
||||
kill $pid1 $pid2 2>/dev/null || true
|
||||
$BINARY stop cluster1.yaml cluster2.yaml >/dev/null 2>&1
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -224,9 +233,9 @@ EOF
|
||||
else
|
||||
log_error "Cluster formation failed (N1 members: $node1_members, N2 members: $node2_members)"
|
||||
fi
|
||||
|
||||
kill $pid1 $pid2 2>/dev/null || true
|
||||
sleep 2
|
||||
|
||||
$BINARY stop cluster1.yaml cluster2.yaml >/dev/null 2>&1
|
||||
sleep 1
|
||||
}
|
||||
|
||||
# Test 4: Conflict resolution (Merkle Tree based)
|
||||
@@ -274,16 +283,15 @@ allow_anonymous_write: true
|
||||
cluster_secret: "$CLUSTER_SECRET"
|
||||
EOF
|
||||
|
||||
# Start nodes
|
||||
# Start nodes using daemon commands
|
||||
# Node 1 started first, making it "older" for tie-breaker if timestamps are equal
|
||||
"$BINARY" conflict1.yaml >conflict1.log 2>&1 &
|
||||
local pid1=$!
|
||||
|
||||
$BINARY start conflict1.yaml >/dev/null 2>&1
|
||||
sleep 2
|
||||
|
||||
if wait_for_service 8111; then
|
||||
$BINARY start conflict2.yaml >/dev/null 2>&1
|
||||
sleep 2
|
||||
$BINARY conflict2.yaml >conflict2.log 2>&1 &
|
||||
local pid2=$!
|
||||
|
||||
|
||||
if wait_for_service 8112; then
|
||||
# Get initial data (full StoredValue)
|
||||
local node1_initial_full=$(curl -s http://localhost:8111/kv/test/conflict/data)
|
||||
@@ -344,8 +352,10 @@ EOF
|
||||
log_error "Resolved data has inconsistent UUID/Timestamp: N1_UUID=$node1_final_uuid, N1_TS=$node1_final_timestamp, N2_UUID=$node2_final_uuid, N2_TS=$node2_final_timestamp"
|
||||
fi
|
||||
|
||||
# Optionally, check logs for conflict resolution messages
|
||||
if grep -q "Conflict resolved" conflict1.log conflict2.log 2>/dev/null; then
|
||||
# Check logs for conflict resolution messages
|
||||
local log1=$(get_log_file conflict1.yaml)
|
||||
local log2=$(get_log_file conflict2.yaml)
|
||||
if grep -q "Conflict resolved" "$log1" "$log2" 2>/dev/null; then
|
||||
log_success "Conflict resolution messages found in logs"
|
||||
else
|
||||
log_error "No 'Conflict resolved' messages found in logs, but data converged."
|
||||
@@ -357,14 +367,14 @@ EOF
|
||||
else
|
||||
log_error "Conflict node 2 failed to start"
|
||||
fi
|
||||
|
||||
kill $pid2 2>/dev/null || true
|
||||
|
||||
$BINARY stop conflict2.yaml >/dev/null 2>&1
|
||||
else
|
||||
log_error "Conflict node 1 failed to start"
|
||||
fi
|
||||
|
||||
kill $pid1 2>/dev/null || true
|
||||
sleep 2
|
||||
|
||||
$BINARY stop conflict1.yaml >/dev/null 2>&1
|
||||
sleep 1
|
||||
else
|
||||
cd "$TEST_DIR"
|
||||
log_error "Failed to create conflict test data. Ensure test_conflict.go is correct."
|
||||
@@ -388,22 +398,21 @@ allow_anonymous_read: false
|
||||
allow_anonymous_write: false
|
||||
EOF
|
||||
|
||||
# Start node
|
||||
$BINARY auth_test.yaml >auth_test.log 2>&1 &
|
||||
local pid=$!
|
||||
|
||||
# Start node using daemon command
|
||||
$BINARY start auth_test.yaml >/dev/null 2>&1
|
||||
sleep 3 # Allow daemon to start and root account creation
|
||||
|
||||
if wait_for_service 8095; then
|
||||
sleep 2 # Allow root account creation
|
||||
|
||||
# Extract the token from logs
|
||||
local token=$(grep "Token:" auth_test.log | sed 's/.*Token: //' | tr -d '\n\r')
|
||||
|
||||
local log_file=$(get_log_file auth_test.yaml)
|
||||
local token=$(grep "Token:" "$log_file" | sed 's/.*Token: //' | tr -d '\n\r')
|
||||
|
||||
if [ -z "$token" ]; then
|
||||
log_error "Failed to extract authentication token from logs"
|
||||
kill $pid 2>/dev/null || true
|
||||
$BINARY stop auth_test.yaml >/dev/null 2>&1
|
||||
return
|
||||
fi
|
||||
|
||||
|
||||
# Test 1: Admin endpoints should fail without authentication
|
||||
local no_auth_response=$(curl -s -X POST http://localhost:8095/api/users -H "Content-Type: application/json" -d '{"nickname":"test","password":"test"}')
|
||||
if echo "$no_auth_response" | grep -q "Unauthorized"; then
|
||||
@@ -411,7 +420,7 @@ EOF
|
||||
else
|
||||
log_error "Admin endpoints should reject unauthenticated requests, got: $no_auth_response"
|
||||
fi
|
||||
|
||||
|
||||
# Test 2: Admin endpoints should work with valid authentication
|
||||
local auth_response=$(curl -s -X POST http://localhost:8095/api/users -H "Content-Type: application/json" -H "Authorization: Bearer $token" -d '{"nickname":"authtest","password":"authtest"}')
|
||||
if echo "$auth_response" | grep -q "uuid"; then
|
||||
@@ -419,7 +428,7 @@ EOF
|
||||
else
|
||||
log_error "Admin endpoints should work with authentication, got: $auth_response"
|
||||
fi
|
||||
|
||||
|
||||
# Test 3: KV endpoints should require auth when anonymous access is disabled
|
||||
local kv_no_auth=$(curl -s -X PUT http://localhost:8095/kv/test/auth -H "Content-Type: application/json" -d '{"test":"auth"}')
|
||||
if echo "$kv_no_auth" | grep -q "Unauthorized"; then
|
||||
@@ -427,7 +436,7 @@ EOF
|
||||
else
|
||||
log_error "KV endpoints should require auth when anonymous access disabled, got: $kv_no_auth"
|
||||
fi
|
||||
|
||||
|
||||
# Test 4: KV endpoints should work with valid authentication
|
||||
local kv_auth=$(curl -s -X PUT http://localhost:8095/kv/test/auth -H "Content-Type: application/json" -H "Authorization: Bearer $token" -d '{"test":"auth"}')
|
||||
if echo "$kv_auth" | grep -q "uuid\|timestamp" || [ -z "$kv_auth" ]; then
|
||||
@@ -435,12 +444,12 @@ EOF
|
||||
else
|
||||
log_error "KV endpoints should work with authentication, got: $kv_auth"
|
||||
fi
|
||||
|
||||
kill $pid 2>/dev/null || true
|
||||
sleep 2
|
||||
|
||||
$BINARY stop auth_test.yaml >/dev/null 2>&1
|
||||
sleep 1
|
||||
else
|
||||
log_error "Auth test node failed to start"
|
||||
kill $pid 2>/dev/null || true
|
||||
$BINARY stop auth_test.yaml >/dev/null 2>&1
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -461,19 +470,18 @@ allow_anonymous_read: false
|
||||
allow_anonymous_write: false
|
||||
EOF
|
||||
|
||||
# Start node
|
||||
$BINARY metadata_test.yaml >metadata_test.log 2>&1 &
|
||||
local pid=$!
|
||||
# Start node using daemon command
|
||||
$BINARY start metadata_test.yaml >/dev/null 2>&1
|
||||
sleep 3 # Allow daemon to start and root account creation
|
||||
|
||||
if wait_for_service 8096; then
|
||||
sleep 2 # Allow root account creation
|
||||
|
||||
# Extract the token from logs
|
||||
local token=$(grep "Token:" metadata_test.log | sed 's/.*Token: //' | tr -d '\n\r')
|
||||
local log_file=$(get_log_file metadata_test.yaml)
|
||||
local token=$(grep "Token:" "$log_file" | sed 's/.*Token: //' | tr -d '\n\r')
|
||||
|
||||
if [ -z "$token" ]; then
|
||||
log_error "Failed to extract authentication token from logs"
|
||||
kill $pid 2>/dev/null || true
|
||||
$BINARY stop metadata_test.yaml >/dev/null 2>&1
|
||||
return
|
||||
fi
|
||||
|
||||
@@ -525,14 +533,78 @@ EOF
|
||||
log_error "Metadata endpoints should require authentication, got code: $no_auth_code"
|
||||
fi
|
||||
|
||||
kill $pid 2>/dev/null || true
|
||||
sleep 2
|
||||
$BINARY stop metadata_test.yaml >/dev/null 2>&1
|
||||
sleep 1
|
||||
else
|
||||
log_error "Metadata test node failed to start"
|
||||
kill $pid 2>/dev/null || true
|
||||
$BINARY stop metadata_test.yaml >/dev/null 2>&1
|
||||
fi
|
||||
}
|
||||
|
||||
# Test 7: Daemon commands (start, stop, status, restart)
|
||||
test_daemon_commands() {
|
||||
test_start "Daemon command tests (start, stop, status, restart)"
|
||||
|
||||
# Create daemon test config
|
||||
cat > daemon_test.yaml <<EOF
|
||||
node_id: "daemon-test"
|
||||
bind_address: "127.0.0.1"
|
||||
port: 8097
|
||||
data_dir: "./daemon_test_data"
|
||||
seed_nodes: []
|
||||
log_level: "error"
|
||||
allow_anonymous_read: true
|
||||
allow_anonymous_write: true
|
||||
EOF
|
||||
|
||||
# Test 1: Start command
|
||||
$BINARY start daemon_test.yaml >/dev/null 2>&1
|
||||
sleep 3 # Allow daemon to start
|
||||
|
||||
if wait_for_service 8097 5; then
|
||||
log_success "Daemon 'start' command works"
|
||||
|
||||
# Test 2: Status command shows running
|
||||
local status_output=$($BINARY status daemon_test.yaml 2>&1)
|
||||
if echo "$status_output" | grep -q "RUNNING"; then
|
||||
log_success "Daemon 'status' command shows RUNNING"
|
||||
else
|
||||
log_error "Daemon 'status' should show RUNNING, got: $status_output"
|
||||
fi
|
||||
|
||||
# Test 3: Stop command
|
||||
$BINARY stop daemon_test.yaml >/dev/null 2>&1
|
||||
sleep 2
|
||||
|
||||
# Check that service is actually stopped
|
||||
if ! curl -s "http://localhost:8097/health" >/dev/null 2>&1; then
|
||||
log_success "Daemon 'stop' command works"
|
||||
else
|
||||
log_error "Daemon should be stopped but is still responding"
|
||||
fi
|
||||
|
||||
# Test 4: Restart command
|
||||
$BINARY restart daemon_test.yaml >/dev/null 2>&1
|
||||
sleep 3
|
||||
|
||||
if wait_for_service 8097 5; then
|
||||
log_success "Daemon 'restart' command works"
|
||||
|
||||
# Clean up
|
||||
$BINARY stop daemon_test.yaml >/dev/null 2>&1
|
||||
sleep 1
|
||||
else
|
||||
log_error "Daemon 'restart' failed to start service"
|
||||
fi
|
||||
else
|
||||
log_error "Daemon 'start' command failed"
|
||||
fi
|
||||
|
||||
# Ensure cleanup
|
||||
pkill -f "daemon_test.yaml" 2>/dev/null || true
|
||||
sleep 1
|
||||
}
|
||||
|
||||
# Main test execution
|
||||
main() {
|
||||
echo "=================================================="
|
||||
@@ -552,6 +624,7 @@ main() {
|
||||
test_conflict_resolution
|
||||
test_authentication_middleware
|
||||
test_metadata_management
|
||||
test_daemon_commands
|
||||
|
||||
# Results
|
||||
echo "=================================================="
|
||||
|
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