gastownhall/beads · warning
sending SIGTERM to PID %d: %w
Error message
sending SIGTERM to PID %d: %w
What it means
gracefulStop wraps process.Signal(syscall.SIGTERM) failures as "sending SIGTERM to PID %d: %w". On Unix, Signal returns os.ErrProcessDone if the target already exited; other errors mean permission problems or the process vanished. This library throws it when it cannot deliver the polite termination signal to the dolt-server process during reclaimPort or StopWithForce (after data was already flushed).
Source
Thrown at internal/doltserver/doltserver_unix.go:146
// Uses signal 0 which doesn't send a signal but checks process existence.
func isProcessAlive(pid int) bool {
process, err := os.FindProcess(pid)
if err != nil {
return false
}
return process.Signal(syscall.Signal(0)) == nil
}
// gracefulStop sends SIGTERM, waits for the process to exit, then SIGKILL if needed.
// Used by reclaimPort and StopWithForce where data has already been flushed.
func gracefulStop(pid int, timeout time.Duration) error {
process, err := os.FindProcess(pid)
if err != nil {
return fmt.Errorf("finding process %d: %w", pid, err)
}
if err := process.Signal(syscall.SIGTERM); err != nil {
return fmt.Errorf("sending SIGTERM to PID %d: %w", pid, err)
}
// Poll for exit
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
time.Sleep(500 * time.Millisecond)
if process.Signal(syscall.Signal(0)) != nil {
return nil // exited
}
}
// Still running — force kill
_ = process.Signal(syscall.SIGKILL)
time.Sleep(100 * time.Millisecond)
return nil
}
View on GitHub (pinned to 71377f2769)
Solutions
- Treat os.ErrProcessDone (errors.Is) as success and proceed — the server is already stopped.
- Check process ownership/permissions: run bd as the same user that started the dolt server (ps -o user= -p <pid>).
- If the process is a zombie/defunct, reap or ignore it and continue; the port should be releasable.
- Fall through to SIGKILL path (StopWithForce already flushed data) or continue startup if the port is free.
Example fix
// before
if err := process.Signal(syscall.SIGTERM); err != nil {
return fmt.Errorf("sending SIGTERM to PID %d: %w", pid, err)
}
// after
if err := process.Signal(syscall.SIGTERM); err != nil {
if errors.Is(err, os.ErrProcessDone) {
return nil // already stopped
}
return fmt.Errorf("sending SIGTERM to PID %d: %w", pid, err)
} Defensive patterns
Strategy: try-catch
Validate before calling
p, err := os.FindProcess(pid)
if err == nil {
if err := p.Signal(syscall.Signal(0)); err != nil {
// cannot signal: wrong user or process gone
}
} Type guard
func canSignal(pid int) bool {
p, err := os.FindProcess(pid)
if err != nil { return false }
return errors.Is(p.Signal(syscall.Signal(0)), syscall.EPERM) == false
} Try / catch
if err := process.Signal(syscall.SIGTERM); err != nil {
if errors.Is(err, os.ErrProcessDone) {
return nil
}
var errno syscall.Errno
if errors.As(err, &errno) && errno == syscall.EPERM {
return fmt.Errorf("insufficient permissions to signal pid %d (wrong user?)", pid)
}
return err
} Prevention
- Check process ownership (ps -o user= -p <pid>) matches the bd user
- Use errors.Is(err, os.ErrProcessDone) to treat exits as success
- Avoid stale PIDs: re-read metadata before signaling
- In containers, signal only PIDs inside the same namespace
When it happens
Trigger: gracefulStop is called and process.Signal(SIGTERM) errors: target process already exited (os.ErrProcessDone), the caller lacks permission to signal the process (different user), or the PID no longer exists.
Common situations: The dolt server crashed between the FindProcess and Signal calls; bd runs as a different user than the stale server process (e.g. after sudo/user change); containerized environments where the PID is outside the namespace.
Related errors
- finding process %d: %w
- finding process %d: %w
- procid: terminate process: %w
- procid: get process exit code: %w
- procid: get process times: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/dec1df4549be27c2.
Report an issue: GitHub.