diff --git a/docs/source/reference-lowlevel.rst b/docs/source/reference-lowlevel.rst index 3e6cc8a4e2..992ff88087 100644 --- a/docs/source/reference-lowlevel.rst +++ b/docs/source/reference-lowlevel.rst @@ -281,27 +281,21 @@ correctness invariants. On the other, if the user accidentally writes an infinite loop, we do want to be able to break out of that. Our solution is to install a default signal handler which checks whether it's safe to raise :exc:`KeyboardInterrupt` at the place where the -signal is received. If so, then we do; otherwise, we schedule a -:exc:`KeyboardInterrupt` to be delivered sometime soon. - -.. note:: Delivery "sometime soon" is accomplished by picking an open - nursery and spawning a new task there that raises - `KeyboardInterrupt`. Like any other unhandled exception, this will - cancel sibling tasks as it propagates, and ultimately escape from - the call to :func:`trio.run` unless caught sooner. - - It's not a good idea to try to catch `KeyboardInterrupt` while - you're still inside Trio, because it might be raised anywhere, - including outside your ``try``/``except`` block. If you want Ctrl+C - to do something that's not "tear down all running tasks", then you - should use :func:`open_signal_receiver` to install a handler for - ``SIGINT``. If you do that, then Ctrl+C will go to your handler rather - than using the default handling described in this section. - - The details of which nursery gets the `KeyboardInterrupt` injected - are subject to change. Currently it's the innermost nursery - that's active in the main task (the one running the original function - you passed to :func:`trio.run`). +signal is received. If so, then we do. Otherwise, we cancel all tasks +and raise `KeyboardInterrupt` directly as the result of :func:`trio.run`. + +.. note:: This behavior means it's not a good idea to try to catch + `KeyboardInterrupt` within a Trio task. Most Trio + programs are I/O-bound, so most interrupts will be received while + no task is running (because Trio is waiting for I/O). There's no + task that should obviously receive the interrupt in such cases, so + Trio doesn't raise it within a task at all: every task gets cancelled, + then `KeyboardInterrupt` is raised once that's complete. + + If you want to handle Ctrl+C by doing something other than "cancel + all tasks", then you should use :func:`open_signal_receiver` to + install a handler for ``SIGINT``. If you do that, then Ctrl+C will + go to your handler, and it can do whatever it wants. So that's great, but – how do we know whether we're in one of the sensitive parts of the program or not? diff --git a/trio/_core/_run.py b/trio/_core/_run.py index 64cc6aa347..41573d9280 100644 --- a/trio/_core/_run.py +++ b/trio/_core/_run.py @@ -1412,48 +1412,12 @@ def current_trio_token(self): def deliver_ki(self): self.ki_pending = True try: - self.entry_queue.run_sync_soon(self._deliver_ki_cb) + self.entry_queue.run_sync_soon( + self.system_nursery.cancel_scope.cancel + ) except RunFinishedError: pass - # The name of this function shows up in tracebacks, so make it a good one - async def _raise_deferred_keyboard_interrupt(self): - raise KeyboardInterrupt - - def _deliver_ki_cb(self): - if not self.ki_pending: - return - - # Can't happen because main_task and run_sync_soon_task are created at - # the same time -- so even if KI arrives before main_task is created, - # we won't get here until afterwards. - assert self.main_task is not None - if self.main_task_outcome is not None: - # We're already in the process of exiting -- leave ki_pending set - # and we'll check it again on our way out of run(). - return - - # Raise KI from a new task in the innermost nursery that was opened - # by the main task. Rationale: - # - Using a new task means we don't have to contend with - # injecting KI at a checkpoint in an existing task. - # - Either the main task has at least one nursery open, or there are - # no non-system tasks except the main task. - # - The main task is likely to be waiting in __aexit__ of its innermost - # nursery. On Trio <=0.15.0, a deferred KI would be raised at the - # main task's next checkpoint. So, spawning our raise-KI task in the - # main task's innermost nursery is the most backwards-compatible - # thing we can do. - for nursery in reversed(self.main_task.child_nurseries): - if not nursery._closed: - self.ki_pending = False - nursery.start_soon(self._raise_deferred_keyboard_interrupt) - return - - # If we get here, the main task has no non-closed child nurseries. - # Cancel the whole run; we'll raise KI on our way out of run(). - self.system_nursery.cancel_scope.cancel() - ################ # Quiescing ################ diff --git a/trio/_core/tests/test_ki.py b/trio/_core/tests/test_ki.py index fcab24135a..ddbf10c8e5 100644 --- a/trio/_core/tests/test_ki.py +++ b/trio/_core/tests/test_ki.py @@ -438,52 +438,6 @@ async def main(): _core.run(main) assert record == [] - print("check 11") - # KI delivered into innermost main task nursery if there are any - @_core.enable_ki_protection - async def main(): - async with _core.open_nursery(): - with pytest.raises(KeyboardInterrupt): - async with _core.open_nursery(): - ki_self() - ki_self() - record.append("ok") - # First tick ensures KI callback ran - # Second tick ensures KI delivery task ran - await _core.cancel_shielded_checkpoint() - await _core.cancel_shielded_checkpoint() - with pytest.raises(_core.Cancelled): - await _core.checkpoint() - record.append("ok 2") - record.append("ok 3") - record.append("ok 4") - - _core.run(main) - assert record == ["ok", "ok 2", "ok 3", "ok 4"] - - # Closed nurseries are ignored when picking one to deliver KI - print("check 12") - record = [] - - @_core.enable_ki_protection - async def main(): - with pytest.raises(KeyboardInterrupt): - async with _core.open_nursery(): - async with _core.open_nursery() as inner: - assert inner._closed is False - inner._closed = True - ki_self() - # First tick ensures KI callback ran - # Second tick ensures KI delivery task ran - await _core.cancel_shielded_checkpoint() - await _core.cancel_shielded_checkpoint() - record.append("ok") - record.append("nope") # pragma: no cover - record.append("ok 2") - - _core.run(main) - assert record == ["ok", "ok 2"] - def test_ki_is_good_neighbor(): # in the unlikely event someone overwrites our signal handler, we leave