Fix Ctrl-C Signal Handling in CLI · Entire

wow that was a poor experience why can't i control-c

✓ Cloning entirehq/entire.io into eukanuba.partial.to Mirror already exists (01KWBK9B0JMY40DTPV1TPW693T) entire://eukanuba.partial.to/gh/entirehq/entire.io

Clone it: git clone entire://eukanuba.partial.to/gh/entirehq/entire.io do request: Post "https://eu.auth.partial.to/api/v1/mirrors": cross-juris transport: base round trip: context canceled

Root causes

1. Repeated Ctrl-C is swallowed — the real "why can't I control-c" (cmd/entire/main.go:33-36):

1
2
3
4

go func() {
    <-sigChan   // reads exactly ONE signal, then this goroutine exits
    cancel()
}

signal.Notify disables Go's default "SIGINT terminates the process" for the whole run. After the first Ctrl-C cancels the context, nothing reads sigChan again, so further Ctrl-C hits the buffer and does nothing. There's no force-quit escalation. You were mashing Ctrl-C into a void.

2. Cancellation prints as errors, not a clean abort. On the one-shot mirror create path, CreateMirror returns the raw ogen error on cancel; renderCoreError passes it straight through; main.go prints it — hence do request: Post ".../mirrors": ... context canceled. The wizard path already handles this (if ctx.Err() != nil { return NewSilentError(...) } at repo_mirror_create_wizard.go:313), but the one-shot and the generic runCoreClient/main.go paths don't swallow context.Canceled.

3. signal: interrupt from the token read — tokenstore on macOS uses zalando/go-keyring, which shells out to /usr/bin/security. callKeyringWithTimeout runs on context.Background() (keyring_timeout.go:47), so your Ctrl-C can't cancel it — it only kills the security child, which surfaces as read access token: signal: interrupt. The repeated lines are in-flight goroutines + your repeated Ctrl-C each reporting their own cancellation.

The fix

Small, standard, high-value: first Ctrl-C cancels + warns, second Ctrl-C force-exits 130, and treat context.Canceled as a silent abort instead of an error dump.

1
2
3
4
5
6
7
8

// main.go
go func() {
    <-sigChan
    fmt.Fprintln(os.Stderr, "\nInterrupting… press Ctrl-C again to force quit.")
    cancel()
    <-sigChan
    os.Exit(130)
}

Plus: in main.go's error switch, exit 130 silently when errors.Is(err, context.Canceled), and have the one-shot mirror path wrap cancel as NewSilentError like the wizard already does.

Yes — and the fix is now in place

Why Ctrl-C couldn't break your loop: entire traps SIGINT and exits normally. A shell only aborts a while true; do entire ...; done loop when the foreground child was killed by SIGINT (WIFSIGNALED) — not when it exits cleanly, even with code 130. So every Ctrl-C just ended one iteration; the loop instantly respawned entire, and you were racing an endless stream of fresh processes. My first patch (os.Exit(130)) was still a normal exit, so it wouldn't have helped your loop either.

The fix: on interrupt, entire now re-raises SIGINT to itself (dieFromInterrupt) so it dies by the signal. I proved it end-to-end with a PTY test driving your shell (zsh): with the fix a single Ctrl-C breaks while true; do work; done (0 iterations after); with the old behavior the loop ran 3 more times.

Clean. Here's the answer.

The full change (cmd/entire/main.go, +40 lines, lint clean)

  1. Re-raise SIGINT on abort → shells break loops on one Ctrl-C.
  2. Double-Ctrl-C escalation → second Ctrl-C force-quits if something's genuinely stuck (e.g. the uncancellable Keychain subprocess read).
  3. Silent context.Canceled → no more raw do request: Post ".../mirrors": ... context canceled / signal: interrupt spew on abort.

Not committed/pushed. Want me to run the full test:ci before you push? (The diff is isolated to main.go, which has no test harness, so I skipped it.)


Done. PR #1604: https://github.com/entireio/cli/pull/1604