Guard against SIGINT during sounddevice atexit teardown#238
Merged
Conversation
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
| if _shutdown_interrupt_guard_installed: | ||
| return | ||
| atexit.register(_ignore_interrupt_during_shutdown) | ||
| _shutdown_interrupt_guard_installed = True |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 setsSIGINTtoSIG_IGNduring shutdown. Wrapped incontextlib.suppress(ValueError)to handle edge cases in non-standard Python embeddings wheresignal.signal()may fail._install_shutdown_interrupt_guard(): New function that registers_ignore_interrupt_during_shutdown()withatexitexactly once (guarded by a process-global_shutdown_interrupt_guard_installedlatch). 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: ConfirmsSIGINTis set toSIG_IGNtest_install_shutdown_interrupt_guard_registers_once: Verifies idempotent registration via the latchtest_import_sounddevice_installs_shutdown_guard: Confirms the guard is installed on successful importtest_import_sounddevice_missing_does_not_register_guard: Confirms the guard is not installed if sounddevice import failsLinting exception: Added
PLW0603(global statement) exception formicrophone.pyinpyproject.tomlto allow the process-global once-latch pattern, consistent with similar state inenvironments.pyanddebuglog.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,
SIGINTis already ignored, so a second Ctrl-C won't raise inside that callback.https://claude.ai/code/session_01LodBoXNkH2RPqAjZFPx3Gj