Skip to content

Fix Ctrl+C handling in CLI - #4411

Merged
un-def merged 1 commit into
masterfrom
pr_ctrl_c_handling
Oct 26, 2026
Merged

un-def merged 1 commit into
masterfrom
pr_ctrl_c_handling

Conversation

@un-def

@un-def un-def commented Oct 26, 2026

Copy link
Copy Markdown
Collaborator

confirm_ask turned Ctrl+C into sys.exit(1). Callers that catch a second Ctrl+C at their prompt never saw it:

  • dstack apply: Ctrl+C at "Stop the run before detaching?" is meant to abort the run. Instead it printed "Cancelled by user", detached and left the run running.
  • dstack apply with a preset: Ctrl+C at "Stop creating preset?" is meant to stop it. Instead the agent (started in its own session) kept running along with its runs.

Besides that, Ctrl+C was handled inconsistently. Some commands exited 0 (apply, preset), some exited 1, and the rest printed a traceback.

Changes

  • main() handles KeyboardInterrupt once. It prints "Interrupted" to stderr and re-raises, with the traceback hidden by an excepthook. On an uncaught KeyboardInterrupt, Python shuts down normally (including atexit, which closes SSH tunnels) and then kills itself with SIGINT. A calling shell script sees that and stops. Exiting with 130 is not the same: bash then assumes the child handled SIGINT itself and continues the script.
  • Removed the local handlers that converted Ctrl+C to an exit code: confirm_ask, dstack login, _repo_diff_verbose (library code, also reachable through the Python API), and the apply and preset catch-alls that exited 0.
  • dstack apply: after aborting a run on a second Ctrl+C, re-raise instead of exiting 0.
  • Questionary menus (dstack login provider, default project) use unsafe_ask(). ask() swallows Ctrl+C and returns None, so dstack login carried on as if nothing had been selected.
  • Fleet provisioning on old servers no longer asks "Delete the fleet before exiting?" with -y, matching the new-server path.

Deliberate stops still exit 0: detaching from a run, the y/n answers at the stop prompts, --watch loops, dstack logs, dstack server (uvicorn swallows the interrupt itself), and the preset commands after their own interrupt handling.

Before/after

Ctrl+C at the dstack login URL prompt, inside for i in 1 2; do dstack login; echo "iteration $i: rc=$?"; done:

Before:

Enter the server URL (https://sky.dstack.ai): ^C
Cancelled by user
iteration 1: rc=1
Enter the server URL (https://sky.dstack.ai): ^C
Cancelled by user
iteration 2: rc=1

After (the loop stops; bash itself exits via SIGINT):

Enter the server URL (https://sky.dstack.ai): ^C
Interrupted

Tests

  • The dstack apply and preset double-Ctrl+C tests drive the real confirm_ask and fail without the fix.
  • test_main.py covers the handler and the excepthook. It also has one subprocess test (~0.6 s, POSIX only) checking that the process exits via SIGINT without a traceback.

🤖 Generated with Claude Code

confirm_ask turned Ctrl+C into sys.exit(1), so handlers that catch a
second Ctrl+C at a prompt never ran: `dstack apply` didn't abort the
run, and preset creation left the agent and its runs going.

Handle KeyboardInterrupt once in main(): print "Interrupted" and
re-raise with the traceback hidden. On an uncaught KeyboardInterrupt,
Python shuts down normally and then kills itself with SIGINT, so a
calling shell script stops too. Exiting with 130 doesn't do that: bash
assumes the child handled SIGINT itself and continues.

- Drop the interrupt handlers in confirm_ask, login, _repo_diff_verbose
  (library code), and the apply and preset catch-alls, which exited 0.
- Re-raise after aborting a run on a second Ctrl+C.
- Let Ctrl+C through questionary menus (unsafe_ask).
- Respect -y when interrupting fleet provisioning on old servers.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@un-def
un-def merged commit c7208bb into master Oct 26, 2026
29 checks passed
@un-def
un-def deleted the pr_ctrl_c_handling branch October 26, 2026 10:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant