fix(windows): daemon stop/kill now uses proper Windows API (GH#992)
On Windows, the daemon stop command was failing with "exit status 1" because `taskkill` without `/F` flag does not work for console processes that do not have windows to receive close messages. Changes: - Use `os.Process.Kill()` which calls Windows `TerminateProcess` API - This is the reliable way to terminate processes on Windows - The graceful RPC shutdown is already attempted before falling back to kill - Updated error messages to be platform-agnostic (removed SIGTERM/SIGKILL) The fix uses Go cross-platform process APIs instead of shelling out to external commands, which is more reliable and portable. Closes #992 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -411,9 +411,9 @@ func stopDaemonWithTimeout(daemon DaemonInfo) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try SIGTERM with 3 second timeout
|
// Try graceful kill with 3 second timeout
|
||||||
if err := killProcess(daemon.PID); err != nil {
|
if err := killProcess(daemon.PID); err != nil {
|
||||||
return fmt.Errorf("SIGTERM failed: %w", err)
|
return fmt.Errorf("kill process failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait up to 3 seconds for process to die
|
// Wait up to 3 seconds for process to die
|
||||||
@@ -424,9 +424,9 @@ func stopDaemonWithTimeout(daemon DaemonInfo) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SIGTERM timeout, try SIGKILL with 1 second timeout
|
// Graceful kill timeout, try force kill with 1 second timeout
|
||||||
if err := forceKillProcess(daemon.PID); err != nil {
|
if err := forceKillProcess(daemon.PID); err != nil {
|
||||||
return fmt.Errorf("SIGKILL failed: %w", err)
|
return fmt.Errorf("force kill failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait up to 1 second for process to die
|
// Wait up to 1 second for process to die
|
||||||
@@ -437,5 +437,5 @@ func stopDaemonWithTimeout(daemon DaemonInfo) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("process %d did not die after SIGKILL", daemon.PID)
|
return fmt.Errorf("process %d did not die after force kill", daemon.PID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,45 +3,70 @@
|
|||||||
package daemon
|
package daemon
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strconv"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func killProcess(pid int) error {
|
func killProcess(pid int) error {
|
||||||
// Use taskkill on Windows (without /F for graceful)
|
// On Windows, there's no SIGTERM equivalent for console processes.
|
||||||
cmd := exec.Command("taskkill", "/PID", strconv.Itoa(pid))
|
// The graceful RPC shutdown is already attempted before this function is called.
|
||||||
if err := cmd.Run(); err != nil {
|
// Use os.Process.Kill() which calls TerminateProcess - the reliable Windows API.
|
||||||
|
proc, err := os.FindProcess(pid)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to find process %d: %w", pid, err)
|
||||||
|
}
|
||||||
|
if err := proc.Kill(); err != nil {
|
||||||
return fmt.Errorf("failed to kill PID %d: %w", pid, err)
|
return fmt.Errorf("failed to kill PID %d: %w", pid, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func forceKillProcess(pid int) error {
|
func forceKillProcess(pid int) error {
|
||||||
// Use taskkill with /F flag for force kill
|
// On Windows, Kill() already uses TerminateProcess which is forceful
|
||||||
cmd := exec.Command("taskkill", "/PID", strconv.Itoa(pid), "/F")
|
return killProcess(pid)
|
||||||
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 {
|
func isProcessAlive(pid int) bool {
|
||||||
// Use tasklist to check if process exists
|
// On Windows, FindProcess always succeeds, but we can check if the process
|
||||||
|
// is actually running by trying to get its exit code via Wait with WNOHANG.
|
||||||
|
// A simpler approach: try to open the process and check for errors.
|
||||||
|
proc, err := os.FindProcess(pid)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// On Windows, we need to actually check if the process exists.
|
||||||
|
// Signal(nil) on Windows returns an error if process doesn't exist.
|
||||||
|
// However, os.Process.Signal is not implemented on Windows.
|
||||||
|
// Use a different approach: try to kill with signal 0 equivalent.
|
||||||
|
// Actually, on Windows we can check via process handle opening.
|
||||||
|
// The simplest reliable way is to use tasklist.
|
||||||
|
//
|
||||||
|
// Note: os.FindProcess on Windows always succeeds regardless of whether
|
||||||
|
// the process exists. We need to actually try to interact with it.
|
||||||
|
// Using Release() and checking the error doesn't work either.
|
||||||
|
//
|
||||||
|
// Fall back to tasklist for reliability.
|
||||||
|
return isProcessAliveTasklist(pid, proc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isProcessAliveTasklist(pid int, _ *os.Process) bool {
|
||||||
|
// Use Windows API via tasklist to check if process exists
|
||||||
|
// This is the most reliable method on Windows
|
||||||
cmd := exec.Command("tasklist", "/FI", fmt.Sprintf("PID eq %d", pid), "/NH")
|
cmd := exec.Command("tasklist", "/FI", fmt.Sprintf("PID eq %d", pid), "/NH")
|
||||||
output, err := cmd.Output()
|
output, err := cmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
// Check if output contains the PID
|
// Check if output contains the PID (tasklist returns "INFO: No tasks..." if not found)
|
||||||
return contains(string(output), strconv.Itoa(pid))
|
return containsSubstring(string(output), fmt.Sprintf("%d", pid))
|
||||||
}
|
}
|
||||||
|
|
||||||
func contains(s, substr string) bool {
|
func containsSubstring(s, substr string) bool {
|
||||||
return findSubstring(s, substr)
|
for i := 0; i <= len(s)-len(substr); i++ {
|
||||||
}
|
if s[i:i+len(substr)] == substr {
|
||||||
|
return true
|
||||||
func findSubstring(s, substr string) bool {
|
}
|
||||||
return len(s) >= len(substr) && bytes.Contains([]byte(s), []byte(substr))
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user