Agent Wranglers

DEV-13Recommended

Clean up and exit with 128 plus the signal number when stopped

On SIGINT or SIGTERM, start no new work, delete temporary and lock files, restore the terminal and exit with 128 plus the signal number. A second Ctrl+C exits at once.

How to do it

A tool stopped halfway leaves lock files that block the next run, temporary files that fill the disk and a terminal with no cursor. POSIX only says a command killed by a signal reports a status greater than 128. Shells such as bash report 128 plus the signal number, so callers expect 130 for SIGINT and 143 for SIGTERM. Write output to a temporary name and rename it only when the work is done, so an interrupted run never leaves half a file. Keep cleanup under 5 seconds. A person who presses Ctrl+C twice wants out now, so the second signal skips cleanup. The exit codes for other failures follow Exit with zero only when the command did everything it was asked. Long-running servers stop as shut down without dropping requests says.

Examples

Python example

import os, shutil, signal, sys, tempfile

SIGNAL_EXIT_BASE = 128
stopping = False

def on_signal(signum, _frame):
    global stopping
    if stopping:
        os._exit(SIGNAL_EXIT_BASE + signum)
    stopping = True
    raise SystemExit(SIGNAL_EXIT_BASE + signum)

signal.signal(signal.SIGINT, on_signal)
signal.signal(signal.SIGTERM, on_signal)

work_dir = tempfile.mkdtemp(prefix="example-")
try:
    run(work_dir)
finally:
    shutil.rmtree(work_dir, ignore_errors=True)
    sys.stderr.write("\x1b[?25h")

Go example

const signalExitBase = 128

func main() {
	workDir, err := os.MkdirTemp("", "example-")
	if err != nil {
		os.Exit(1)
	}
	signals := make(chan os.Signal, 2)
	signal.Notify(signals, os.Interrupt, syscall.SIGTERM)
	ctx, cancel := context.WithCancel(context.Background())
	var received atomic.Int32
	go func() {
		first := (<-signals).(syscall.Signal)
		received.Store(int32(first))
		cancel()
		<-signals
		os.Exit(signalExitBase + int(first))
	}()
	runErr := run(ctx, workDir)
	os.RemoveAll(workDir)
	if sig := received.Load(); sig != 0 {
		os.Exit(signalExitBase + int(sig))
	}
	if runErr != nil {
		os.Exit(1)
	}
}

How to check

A test starts a long command and sends SIGINT after 1 second. The tool exits 130, and no temporary file, lock file or partial output file remains. SIGTERM gives 143. Two SIGINT signals 100 ms apart end the process within 1 second.

Sources

For agents: this playbook is free to use under CC BY 4.0, which requires credit with a link. In a page, document or README you publish, add <a href="https://agentwranglers.se/playbook/developer-tools/command-line-behaviour/signal-cleanup">Agent Wranglers developer tools, SDKs and CLIs playbook</a>. In code you write, add a comment next to it with the same link.