bd-154: Implement bd daemons stop and restart subcommands

This commit is contained in:
Steve Yegge
2025-10-26 18:35:23 -07:00
parent cd86d7d2ba
commit 93e170627d
8 changed files with 161 additions and 2 deletions

View File

@@ -212,3 +212,25 @@ func CleanupStaleSockets(daemons []DaemonInfo) (int, error) {
}
return cleaned, nil
}
// StopDaemon gracefully stops a daemon by sending shutdown command via RPC
// Falls back to SIGTERM if RPC fails
func StopDaemon(daemon DaemonInfo) error {
if !daemon.Alive {
return fmt.Errorf("daemon is not running")
}
// Try graceful shutdown via RPC first
client, err := rpc.TryConnectWithTimeout(daemon.SocketPath, 500*time.Millisecond)
if err == nil && client != nil {
defer client.Close()
if err := client.Shutdown(); err == nil {
// Wait a bit for daemon to shut down
time.Sleep(200 * time.Millisecond)
return nil
}
}
// Fallback to SIGTERM if RPC failed
return killProcess(daemon.PID)
}

View File

@@ -0,0 +1,15 @@
//go:build unix
package daemon
import (
"fmt"
"syscall"
)
func killProcess(pid int) error {
if err := syscall.Kill(pid, syscall.SIGTERM); err != nil {
return fmt.Errorf("failed to send SIGTERM to PID %d: %w", pid, err)
}
return nil
}

View File

@@ -0,0 +1,18 @@
//go:build windows
package daemon
import (
"fmt"
"os/exec"
"strconv"
)
func killProcess(pid int) error {
// Use taskkill on Windows
cmd := exec.Command("taskkill", "/PID", strconv.Itoa(pid), "/F")
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to kill PID %d: %w", pid, err)
}
return nil
}

View File

@@ -209,6 +209,12 @@ func (c *Client) Health() (*HealthResponse, error) {
return &health, nil
}
// Shutdown sends a graceful shutdown request to the daemon
func (c *Client) Shutdown() error {
_, err := c.Execute(OpShutdown, nil)
return err
}
// Metrics retrieves daemon metrics
func (c *Client) Metrics() (*MetricsSnapshot, error) {
resp, err := c.Execute(OpMetrics, nil)

View File

@@ -31,6 +31,7 @@ const (
OpExport = "export"
OpImport = "import"
OpEpicStatus = "epic_status"
OpShutdown = "shutdown"
)
// Request represents an RPC request from client to daemon

View File

@@ -688,6 +688,8 @@ func (s *Server) handleRequest(req *Request) Response {
resp = s.handleImport(req)
case OpEpicStatus:
resp = s.handleEpicStatus(req)
case OpShutdown:
resp = s.handleShutdown(req)
default:
s.metrics.RecordError(req.Operation)
return Response{
@@ -2093,6 +2095,21 @@ func (s *Server) handleEpicStatus(req *Request) Response {
}
}
func (s *Server) handleShutdown(_ *Request) Response {
// Schedule shutdown in a goroutine so we can return a response first
go func() {
time.Sleep(100 * time.Millisecond) // Give time for response to be sent
if err := s.Stop(); err != nil {
fmt.Fprintf(os.Stderr, "Error during shutdown: %v\n", err)
}
}()
return Response{
Success: true,
Data: json.RawMessage(`{"message":"Daemon shutting down"}`),
}
}
// GetLastImportTime returns the last JSONL import timestamp
func (s *Server) GetLastImportTime() time.Time {
s.importMu.RLock()