Implement bd daemons killall and logs (bd-153, bd-152)
Amp-Thread-ID: https://ampcode.com/threads/T-f1cff202-188b-4850-a909-c2750d24ad22 Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
@@ -234,3 +234,100 @@ func StopDaemon(daemon DaemonInfo) error {
|
||||
// Fallback to SIGTERM if RPC failed
|
||||
return killProcess(daemon.PID)
|
||||
}
|
||||
|
||||
// KillAllFailure represents a failure to kill a specific daemon
|
||||
type KillAllFailure struct {
|
||||
Workspace string `json:"workspace"`
|
||||
PID int `json:"pid"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
// KillAllResults contains results from KillAllDaemons
|
||||
type KillAllResults struct {
|
||||
Stopped int `json:"stopped"`
|
||||
Failed int `json:"failed"`
|
||||
Failures []KillAllFailure `json:"failures,omitempty"`
|
||||
}
|
||||
|
||||
// KillAllDaemons stops all provided daemons, using force if RPC/SIGTERM fail
|
||||
func KillAllDaemons(daemons []DaemonInfo, force bool) KillAllResults {
|
||||
results := KillAllResults{
|
||||
Failures: []KillAllFailure{},
|
||||
}
|
||||
|
||||
for _, daemon := range daemons {
|
||||
if !daemon.Alive {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := stopDaemonWithTimeout(daemon); err != nil {
|
||||
if force {
|
||||
// Try force kill
|
||||
if err := forceKillProcess(daemon.PID); err != nil {
|
||||
results.Failed++
|
||||
results.Failures = append(results.Failures, KillAllFailure{
|
||||
Workspace: daemon.WorkspacePath,
|
||||
PID: daemon.PID,
|
||||
Error: err.Error(),
|
||||
})
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
results.Failed++
|
||||
results.Failures = append(results.Failures, KillAllFailure{
|
||||
Workspace: daemon.WorkspacePath,
|
||||
PID: daemon.PID,
|
||||
Error: err.Error(),
|
||||
})
|
||||
continue
|
||||
}
|
||||
}
|
||||
results.Stopped++
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// stopDaemonWithTimeout tries RPC shutdown, then SIGTERM with timeout, then SIGKILL
|
||||
func stopDaemonWithTimeout(daemon DaemonInfo) error {
|
||||
// Try RPC shutdown first (2 second timeout)
|
||||
client, err := rpc.TryConnectWithTimeout(daemon.SocketPath, 2*time.Second)
|
||||
if err == nil && client != nil {
|
||||
defer client.Close()
|
||||
if err := client.Shutdown(); err == nil {
|
||||
// Wait and verify process died
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
if !isProcessAlive(daemon.PID) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try SIGTERM with 3 second timeout
|
||||
if err := killProcess(daemon.PID); err != nil {
|
||||
return fmt.Errorf("SIGTERM failed: %w", err)
|
||||
}
|
||||
|
||||
// Wait up to 3 seconds for process to die
|
||||
for i := 0; i < 30; i++ {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
if !isProcessAlive(daemon.PID) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// SIGTERM timeout, try SIGKILL with 1 second timeout
|
||||
if err := forceKillProcess(daemon.PID); err != nil {
|
||||
return fmt.Errorf("SIGKILL failed: %w", err)
|
||||
}
|
||||
|
||||
// Wait up to 1 second for process to die
|
||||
for i := 0; i < 10; i++ {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
if !isProcessAlive(daemon.PID) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("process %d did not die after SIGKILL", daemon.PID)
|
||||
}
|
||||
|
||||
@@ -13,3 +13,16 @@ func killProcess(pid int) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func forceKillProcess(pid int) error {
|
||||
if err := syscall.Kill(pid, syscall.SIGKILL); err != nil {
|
||||
return fmt.Errorf("failed to send SIGKILL to PID %d: %w", pid, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isProcessAlive(pid int) bool {
|
||||
// On Unix, sending signal 0 checks if process exists
|
||||
err := syscall.Kill(pid, 0)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
@@ -3,16 +3,46 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func killProcess(pid int) error {
|
||||
// Use taskkill on Windows
|
||||
cmd := exec.Command("taskkill", "/PID", strconv.Itoa(pid), "/F")
|
||||
// Use taskkill on Windows (without /F for graceful)
|
||||
cmd := exec.Command("taskkill", "/PID", strconv.Itoa(pid))
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to kill PID %d: %w", pid, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func forceKillProcess(pid int) error {
|
||||
// Use taskkill with /F flag for force kill
|
||||
cmd := exec.Command("taskkill", "/PID", strconv.Itoa(pid), "/F")
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to force kill PID %d: %w", pid, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isProcessAlive(pid int) bool {
|
||||
// Use tasklist to check if process exists
|
||||
cmd := exec.Command("tasklist", "/FI", fmt.Sprintf("PID eq %d", pid), "/NH")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
// Check if output contains the PID
|
||||
return contains(string(output), strconv.Itoa(pid))
|
||||
}
|
||||
|
||||
func contains(s, substr string) bool {
|
||||
return findSubstring(s, substr)
|
||||
}
|
||||
|
||||
func findSubstring(s, substr string) bool {
|
||||
return len(s) >= len(substr) && bytes.Contains([]byte(s), []byte(substr))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user