Skip to content

Commit

Permalink
Avoid signal handling reentrancy issues
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
RobinJadoul authored and pazz committed May 3, 2024
1 parent 42a686c commit f9eb913
Showing 1 changed file with 13 additions and 6 deletions.
19 changes: 13 additions & 6 deletions alot/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ def __init__(self, dbman, initialcmdline):
mainframe = urwid.Frame(urwid.SolidFill())
self.root_widget = urwid.AttrMap(mainframe, global_att)

signal.signal(signal.SIGINT, self.handle_signal)
signal.signal(signal.SIGUSR1, self.handle_signal)
signal.signal(signal.SIGINT, self._handle_signal)
signal.signal(signal.SIGUSR1, self._handle_signal)

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

def handle_signal(self, signum, frame):
def _handle_signal(self, signum, _frame):
"""
Handle UNIX signals: add a new task onto the event loop.
Doing it this way ensures what our handler has access to whatever
synchronization primitives or async calls it may require.
"""
loop = asyncio.get_event_loop()
asyncio.run_coroutine_threadsafe(self.handle_signal(signum), loop)

async def handle_signal(self, signum):
"""
handles UNIX signals
This function currently just handles SIGUSR1. It could be extended to
handle more
:param signum: The signal number (see man 7 signal)
:param frame: The execution frame
(https://docs.python.org/2/reference/datamodel.html#frame-objects)
"""
# it is a SIGINT ?
if signum == signal.SIGINT:
logging.info('shut down cleanly')
asyncio.ensure_future(self.apply_command(globals.ExitCommand()))
await self.apply_command(globals.ExitCommand())
elif signum == signal.SIGUSR1:
if isinstance(self.current_buffer, SearchBuffer):
self.current_buffer.rebuild()
Expand Down

0 comments on commit f9eb913

Please sign in to comment.