hashicorp/nomad · warning

Error sending ctrl-break event: %v

Error message

Error sending ctrl-break event: %v

What it means

sendCtrlBreak wraps the failure of windows.GenerateConsoleCtrlEvent, which delivers CTRL_BREAK_EVENT to a task process so it can shut down gracefully. This error means the console control event could not be delivered to the given PID.

Source

Thrown at drivers/shared/executor/executor_windows.go:181

	result, err := syscall.WaitForSingleObject(syscall.Handle(handle), 0)

	switch result {
	case syscall.WAIT_OBJECT_0:
		return nil
	case syscall.WAIT_TIMEOUT:
		// Process still running.  Just kill it.
		return proc.Kill()
	default:
		return os.NewSyscallError("WaitForSingleObject", err)
	}
}

// Send a Ctrl-Break signal for shutting down the process,
func sendCtrlBreak(pid int) error {
	err := windows.GenerateConsoleCtrlEvent(syscall.CTRL_BREAK_EVENT, uint32(pid))
	if err != nil {
		return fmt.Errorf("Error sending ctrl-break event: %v", err)
	}
	return nil
}

// Send the process a Ctrl-Break event, allowing it to shutdown by itself
// before being Terminate.
func (e *UniversalExecutor) shutdownProcess(_ os.Signal, proc *os.Process) error {
	if err := sendCtrlBreak(proc.Pid); err != nil {
		return fmt.Errorf("executor shutdown error: %v", err)
	}
	e.logger.Debug("sent Ctrl-Break to process", "pid", proc.Pid)

	return nil
}

View on GitHub (pinned to 482b49bf1a)

Solutions

  1. Check whether the target process is still alive before/while signaling (accept the error if the process already exited)
  2. Ensure the executor spawned the process with an attached console and new process group so CTRL_BREAK_EVENT is deliverable
  3. Fall back to process termination (Kill) which is the documented next step in shutdownProcess handling
  4. Retry the signal once; transient failures can occur during process teardown

Example fix

// before
if err := sendCtrlBreak(proc.Pid); err != nil { return err }
// after
if err := sendCtrlBreak(proc.Pid); err != nil {
    logger.Warn("ctrl-break failed, killing", "pid", proc.Pid, "err", err)
    return proc.Kill()
}
Defensive patterns

Strategy: try-catch

Validate before calling

alive, err := isProcessAlive(pid); if !alive { skip ctrl-break }

Try / catch

err := executor.Signal(syscall.SIGBREAK)
if err != nil && strings.Contains(err.Error(), "ctrl-break") {
    // fall back to force kill / ignore if process already exited
}

Prevention

When it happens

Trigger: Calling sendCtrlBreak with a PID whose process has exited, has no attached console, or was not started with CREATE_NEW_PROCESS_GROUP / shares no console with the caller.

Common situations: Shutting down an already-dead task during Nomad kill/stop flows; processes started detached from the console; races where the task exits between signal decision and delivery.

Related errors


AI-assisted analysis of hashicorp/nomad@482b49bf1a (2026-09-04). Data as JSON: /api/errors/218e22067d401f70. Report an issue: GitHub.