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

  1. Treat os.ErrProcessDone (errors.Is) as success and proceed — the server is already stopped.
  2. Check process ownership/permissions: run bd as the same user that started the dolt server (ps -o user= -p <pid>).
  3. If the process is a zombie/defunct, reap or ignore it and continue; the port should be releasable.
  4. 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

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


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/dec1df4549be27c2. Report an issue: GitHub.