package infra import ( "fmt" "syscall" "time" ) // ProcessKill sends SIGTERM to the process group of handle, then waits up to // graceSec seconds for the process to exit. If it is still alive after the // grace period, SIGKILL is sent. Returns an error only if the signal could not // be delivered (e.g. the process group does not exist). func ProcessKill(handle *ProcessHandle, graceSec int) error { // Send SIGTERM to the process group (negative pid targets the group). if err := syscall.Kill(-handle.Pid, syscall.SIGTERM); err != nil { // ESRCH means the process is already gone — not an error from our view. if err != syscall.ESRCH { return fmt.Errorf("process_kill: sigterm: %w", err) } return nil } // Poll until the process exits or the grace period expires. deadline := time.Now().Add(time.Duration(graceSec) * time.Second) for time.Now().Before(deadline) { // Check if process has exited by sending signal 0 (no-op). err := syscall.Kill(-handle.Pid, 0) if err == syscall.ESRCH { // Process group is gone. return nil } time.Sleep(100 * time.Millisecond) } // Still alive after grace period — escalate to SIGKILL. if err := syscall.Kill(-handle.Pid, syscall.SIGKILL); err != nil { if err != syscall.ESRCH { return fmt.Errorf("process_kill: sigkill: %w", err) } } return nil }