Add fdm_signal_add() and fdm_signal_del(). Signals added to the fdm
will be monitored, and the provided callback called as “soon as
possible” from the main context (i.e not from the signal handler
context).
Monitored signals are *blocked* by default. We use epoll_pwait() to
unblock them while we’re polling. This allows us to do race-free
signal detection.
We use a single handler for all monitored signals; the handler simply
updates the signal’s slot in a global array (sized to fit SIGRTMAX
signals).
When epoll_pwait() returns EINTR, we loop the global array. The
callback associated with each signal that fired is called.
There are now three hook priorities: low, normal, high.
High priority hooks are executed *first*. Normal next, and last the
low priority hooks.
The renderer's terminal refresh hook now runs as a normal priority
hook.
It is perfectly possible, and legal, for a FDM handler to delete
another handler. The problem however is when the epoll returned array
of FD events contain the removed FD handler *after* the handler that
removed it.
That is, if the epoll returned array is:
[FD=13, FD=37]
and the handler for FD=13 removes FD=37, then given the current
implementation, the FD user data (our handler callback etc) will point
to a free:d address.
Add support for this situation by deferring FD handler removal *when
called from another handler*.
This is done by "locking" the FDM struct before looping the handlers
for FDs with events (and unlocking afterwards).
In fdm_del(), we check if the FDM has been locked, in which case the
FD is marked as deleted, and put on a deferred list. But
not *actually* deleted.
Meanwhile, in the FDM poll function, we skip calling handlers for
marked-for-deletion FDs.
Then, when all FDs have been processed, we loop the deferred list and
finally deletes the FDs for real.