Skip to content

Commit 7ca3089

Browse files
authored
Add the possibility to ignore sigint from other threads (#7623)
### Problem In the context of #7596, pants tasks will be executed outside of the main thread. This means that tasks like python-repl cannot override signal handling (code), because this can only be done from the main thread. ### Solution Create an atomic variable, SignalHandler._ignore_sigint, that gates SIGINT handling. Create a global context manager inside ExceptionSink that toggles this for a certain time if needed. ### Result Any thread can pause the handling of SIGINT in a controlled way. python-repl no longer installs signal handlers, but rather it toggles the handling of the signal. ### Caveat We may want to gate all signals individually, but the semantics of SIGINT are usually different enough from SIGTERM and SIGKILL that I think it's okay to only gate the first one.
1 parent 2af5053 commit 7ca3089

File tree

1 file changed

+51
-2
lines changed

1 file changed

+51
-2
lines changed

src/python/pants/base/exception_sink.py

+51-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import os
1111
import signal
1212
import sys
13+
import threading
1314
import traceback
1415
from builtins import object, str
1516
from contextlib import contextmanager
@@ -43,11 +44,38 @@ def signal_handler_mapping(self):
4344
# instead just iterating over the registered signals to set handlers, so a dict is probably
4445
# better.
4546
return {
46-
signal.SIGINT: self.handle_sigint,
47+
signal.SIGINT: self._handle_sigint_if_enabled,
4748
signal.SIGQUIT: self.handle_sigquit,
4849
signal.SIGTERM: self.handle_sigterm,
4950
}
5051

52+
def __init__(self):
53+
self._ignore_sigint_lock = threading.Lock()
54+
self._threads_ignoring_sigint = 0
55+
56+
def _check_sigint_gate_is_correct(self):
57+
assert self._threads_ignoring_sigint >= 0, \
58+
"This should never happen, someone must have modified the counter outside of SignalHandler."
59+
60+
def _handle_sigint_if_enabled(self, signum, _frame):
61+
with self._ignore_sigint_lock:
62+
self._check_sigint_gate_is_correct()
63+
threads_ignoring_sigint = self._threads_ignoring_sigint
64+
if threads_ignoring_sigint == 0:
65+
self.handle_sigint(signum, _frame)
66+
67+
@contextmanager
68+
def _ignoring_sigint(self):
69+
with self._ignore_sigint_lock:
70+
self._check_sigint_gate_is_correct()
71+
self._threads_ignoring_sigint += 1
72+
try:
73+
yield
74+
finally:
75+
with self._ignore_sigint_lock:
76+
self._threads_ignoring_sigint -= 1
77+
self._check_sigint_gate_is_correct()
78+
5179
def handle_sigint(self, signum, _frame):
5280
raise KeyboardInterrupt('User interrupted execution with control-c!')
5381

@@ -283,6 +311,8 @@ def reset_signal_handler(cls, signal_handler):
283311
OS state:
284312
- Overwrites signal handlers for SIGINT, SIGQUIT, and SIGTERM.
285313
314+
NB: This method calls signal.signal(), which will crash if not called from the main thread!
315+
286316
:returns: The :class:`SignalHandler` that was previously registered, or None if this is
287317
the first time this method was called.
288318
"""
@@ -303,13 +333,32 @@ def reset_signal_handler(cls, signal_handler):
303333
@classmethod
304334
@contextmanager
305335
def trapped_signals(cls, new_signal_handler):
306-
"""A contextmanager which temporarily overrides signal handling."""
336+
"""
337+
A contextmanager which temporarily overrides signal handling.
338+
339+
NB: This method calls signal.signal(), which will crash if not called from the main thread!
340+
"""
307341
try:
308342
previous_signal_handler = cls.reset_signal_handler(new_signal_handler)
309343
yield
310344
finally:
311345
cls.reset_signal_handler(previous_signal_handler)
312346

347+
@classmethod
348+
@contextmanager
349+
def ignoring_sigint(cls):
350+
"""
351+
A contextmanager which disables handling sigint in the current signal handler.
352+
This allows threads that are not the main thread to ignore sigint.
353+
354+
NB: Only use this if you can't use ExceptionSink.trapped_signals().
355+
356+
Class state:
357+
- Toggles `self._ignore_sigint` in `cls._signal_handler`.
358+
"""
359+
with cls._signal_handler._ignoring_sigint():
360+
yield
361+
313362
@classmethod
314363
def _iso_timestamp_for_now(cls):
315364
return datetime.datetime.now().isoformat()

0 commit comments

Comments
 (0)