Skip to content

Guard against SIGINT during sounddevice atexit teardown#238

Merged
alexkroman merged 1 commit into
mainfrom
claude/tender-clarke-hxn5xe
Jun 18, 2026
Merged

Guard against SIGINT during sounddevice atexit teardown#238
alexkroman merged 1 commit into
mainfrom
claude/tender-clarke-hxn5xe

Conversation

@alexkroman

Copy link
Copy Markdown
Collaborator

Summary

Prevents a noisy "Exception ignored in atexit callback" traceback when the user presses Ctrl-C a second time during interpreter shutdown while sounddevice's PortAudio teardown is running.

Changes

  • _ignore_interrupt_during_shutdown(): New function that sets SIGINT to SIG_IGN during shutdown. Wrapped in contextlib.suppress(ValueError) to handle edge cases in non-standard Python embeddings where signal.signal() may fail.

  • _install_shutdown_interrupt_guard(): New function that registers _ignore_interrupt_during_shutdown() with atexit exactly once (guarded by a process-global _shutdown_interrupt_guard_installed latch). Registered after sounddevice imports so atexit's LIFO order ensures our handler runs before sounddevice's PortAudio teardown.

  • import_sounddevice(): Now calls _install_shutdown_interrupt_guard() after a successful import, ensuring the guard is in place whenever sounddevice is available.

  • Test coverage: Four new tests verify the guard's behavior:

    • test_ignore_interrupt_during_shutdown_sets_sig_ign: Confirms SIGINT is set to SIG_IGN
    • test_install_shutdown_interrupt_guard_registers_once: Verifies idempotent registration via the latch
    • test_import_sounddevice_installs_shutdown_guard: Confirms the guard is installed on successful import
    • test_import_sounddevice_missing_does_not_register_guard: Confirms the guard is not installed if sounddevice import fails
  • Linting exception: Added PLW0603 (global statement) exception for microphone.py in pyproject.toml to allow the process-global once-latch pattern, consistent with similar state in environments.py and debuglog.py.

Implementation Details

The guard leverages atexit's LIFO callback order: since sounddevice registers its own atexit handler during import, registering our handler after that import ensures ours runs first (last registered = first executed). By the time sounddevice's PortAudio teardown runs, SIGINT is already ignored, so a second Ctrl-C won't raise inside that callback.

https://claude.ai/code/session_01LodBoXNkH2RPqAjZFPx3Gj

A second Ctrl-C while exiting an audio command (agent/stream/speak/etc.)
landed inside sounddevice's atexit handler — which calls Pa_Terminate —
raising KeyboardInterrupt *inside* the atexit callback. Python then printed
the confusing "Exception ignored in atexit callback" traceback, even though
the first Ctrl-C had already stopped the session cleanly.

Register an atexit guard right after the sounddevice import so atexit's LIFO
order runs it before sounddevice's PortAudio teardown, ignoring any further
SIGINT for the rest of interpreter shutdown. There's nothing left to cancel
once we're exiting.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LodBoXNkH2RPqAjZFPx3Gj
@alexkroman alexkroman enabled auto-merge June 18, 2026 04:43
if _shutdown_interrupt_guard_installed:
return
atexit.register(_ignore_interrupt_during_shutdown)
_shutdown_interrupt_guard_installed = True
@alexkroman alexkroman added this pull request to the merge queue Jun 18, 2026
Merged via the queue into main with commit efdab36 Jun 18, 2026
19 checks passed
@alexkroman alexkroman deleted the claude/tender-clarke-hxn5xe branch June 18, 2026 04:50
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.

2 participants