Skip to content

Commit f9eb913

Browse files
RobinJadoulpazz
authored andcommitted
Avoid signal handling reentrancy issues
If e.g. two SIGUSR1 signals arrive at almost the same time, it can be that the first one hasn't finished refreshing the UI yet when the second one needs to be handled, leading to a generator being executed twice, which python doesn't like. We try to avoid this by scheduling the actual handling logic as a coroutine on the currently running event loop. This has the advantage that the signal handler now has full access to any async functions or synchronization primitives. As long as the event loop is single-threaded, the current version should avoid this particular crash as the refreshing logic is not async and hence cannot be interrupted at the event loop level. If this were to change at some point, an asyncio.Semaphore or equivalent primitive can be easily introduced.
1 parent 42a686c commit f9eb913

File tree

1 file changed

+13
-6
lines changed

1 file changed

+13
-6
lines changed

alot/ui.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,8 @@ def __init__(self, dbman, initialcmdline):
9595
mainframe = urwid.Frame(urwid.SolidFill())
9696
self.root_widget = urwid.AttrMap(mainframe, global_att)
9797

98-
signal.signal(signal.SIGINT, self.handle_signal)
99-
signal.signal(signal.SIGUSR1, self.handle_signal)
98+
signal.signal(signal.SIGINT, self._handle_signal)
99+
signal.signal(signal.SIGUSR1, self._handle_signal)
100100

101101
# load histories
102102
self._cache = os.path.join(
@@ -728,21 +728,28 @@ async def apply_command(self, cmd):
728728
logging.info('calling post-hook')
729729
await cmd.posthook(ui=self, dbm=self.dbman, cmd=cmd)
730730

731-
def handle_signal(self, signum, frame):
731+
def _handle_signal(self, signum, _frame):
732+
"""
733+
Handle UNIX signals: add a new task onto the event loop.
734+
Doing it this way ensures what our handler has access to whatever
735+
synchronization primitives or async calls it may require.
736+
"""
737+
loop = asyncio.get_event_loop()
738+
asyncio.run_coroutine_threadsafe(self.handle_signal(signum), loop)
739+
740+
async def handle_signal(self, signum):
732741
"""
733742
handles UNIX signals
734743
735744
This function currently just handles SIGUSR1. It could be extended to
736745
handle more
737746
738747
:param signum: The signal number (see man 7 signal)
739-
:param frame: The execution frame
740-
(https://docs.python.org/2/reference/datamodel.html#frame-objects)
741748
"""
742749
# it is a SIGINT ?
743750
if signum == signal.SIGINT:
744751
logging.info('shut down cleanly')
745-
asyncio.ensure_future(self.apply_command(globals.ExitCommand()))
752+
await self.apply_command(globals.ExitCommand())
746753
elif signum == signal.SIGUSR1:
747754
if isinstance(self.current_buffer, SearchBuffer):
748755
self.current_buffer.rebuild()

0 commit comments

Comments
 (0)