mirror of
https://codeberg.org/dnkl/foot.git
synced 2026-02-04 04:06:06 -05:00
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.
473 lines
11 KiB
C
473 lines
11 KiB
C
#include "fdm.h"
|
|
|
|
#include <stdlib.h>
|
|
#include <stdbool.h>
|
|
#include <inttypes.h>
|
|
#include <unistd.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <signal.h>
|
|
|
|
#include <sys/epoll.h>
|
|
|
|
#include <tllist.h>
|
|
|
|
#define LOG_MODULE "fdm"
|
|
#define LOG_ENABLE_DBG 0
|
|
#include "log.h"
|
|
#include "debug.h"
|
|
#include "xmalloc.h"
|
|
|
|
struct fd_handler {
|
|
int fd;
|
|
int events;
|
|
fdm_fd_handler_t callback;
|
|
void *callback_data;
|
|
bool deleted;
|
|
};
|
|
|
|
struct sig_handler {
|
|
int signo;
|
|
fdm_signal_handler_t callback;
|
|
void *callback_data;
|
|
};
|
|
|
|
struct hook {
|
|
fdm_hook_t callback;
|
|
void *callback_data;
|
|
};
|
|
|
|
typedef tll(struct hook) hooks_t;
|
|
|
|
struct fdm {
|
|
int epoll_fd;
|
|
bool is_polling;
|
|
tll(struct fd_handler *) fds;
|
|
tll(struct fd_handler *) deferred_delete;
|
|
|
|
sigset_t sigmask;
|
|
struct sig_handler *signal_handlers;
|
|
|
|
hooks_t hooks_low;
|
|
hooks_t hooks_normal;
|
|
hooks_t hooks_high;
|
|
};
|
|
|
|
static volatile sig_atomic_t *received_signals = NULL;
|
|
|
|
struct fdm *
|
|
fdm_init(void)
|
|
{
|
|
sigset_t sigmask;
|
|
if (sigprocmask(0, NULL, &sigmask) < 0) {
|
|
LOG_ERRNO("failed to get process signal mask");
|
|
return NULL;
|
|
}
|
|
|
|
int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
|
|
if (epoll_fd == -1) {
|
|
LOG_ERRNO("failed to create epoll FD");
|
|
return NULL;
|
|
}
|
|
|
|
xassert(received_signals == NULL); /* Only one FDM instance supported */
|
|
received_signals = xcalloc(SIGRTMAX, sizeof(received_signals[0]));
|
|
|
|
struct fdm *fdm = malloc(sizeof(*fdm));
|
|
if (unlikely(fdm == NULL)) {
|
|
LOG_ERRNO("malloc() failed");
|
|
return NULL;
|
|
}
|
|
|
|
struct sig_handler *sig_handlers = calloc(SIGRTMAX, sizeof(sig_handlers[0]));
|
|
|
|
if (sig_handlers == NULL) {
|
|
LOG_ERRNO("failed to allocate signal handler array");
|
|
free(fdm);
|
|
return NULL;
|
|
}
|
|
|
|
*fdm = (struct fdm){
|
|
.epoll_fd = epoll_fd,
|
|
.is_polling = false,
|
|
.fds = tll_init(),
|
|
.deferred_delete = tll_init(),
|
|
.sigmask = sigmask,
|
|
.signal_handlers = sig_handlers,
|
|
.hooks_low = tll_init(),
|
|
.hooks_normal = tll_init(),
|
|
.hooks_high = tll_init(),
|
|
};
|
|
return fdm;
|
|
}
|
|
|
|
void
|
|
fdm_destroy(struct fdm *fdm)
|
|
{
|
|
if (fdm == NULL)
|
|
return;
|
|
|
|
if (tll_length(fdm->fds) > 0)
|
|
LOG_WARN("FD list not empty");
|
|
|
|
for (int i = 0; i < SIGRTMAX; i++) {
|
|
if (fdm->signal_handlers[i].callback != NULL)
|
|
LOG_WARN("handler for signal %d not removed", i);
|
|
}
|
|
|
|
if (tll_length(fdm->hooks_low) > 0 ||
|
|
tll_length(fdm->hooks_normal) > 0 ||
|
|
tll_length(fdm->hooks_high) > 0)
|
|
{
|
|
LOG_WARN("hook list not empty");
|
|
}
|
|
|
|
xassert(tll_length(fdm->fds) == 0);
|
|
xassert(tll_length(fdm->deferred_delete) == 0);
|
|
xassert(tll_length(fdm->hooks_low) == 0);
|
|
xassert(tll_length(fdm->hooks_normal) == 0);
|
|
xassert(tll_length(fdm->hooks_high) == 0);
|
|
|
|
sigprocmask(SIG_SETMASK, &fdm->sigmask, NULL);
|
|
free(fdm->signal_handlers);
|
|
|
|
tll_free(fdm->fds);
|
|
tll_free(fdm->deferred_delete);
|
|
tll_free(fdm->hooks_low);
|
|
tll_free(fdm->hooks_normal);
|
|
tll_free(fdm->hooks_high);
|
|
close(fdm->epoll_fd);
|
|
free(fdm);
|
|
|
|
free((void *)received_signals);
|
|
received_signals = NULL;
|
|
}
|
|
|
|
bool
|
|
fdm_add(struct fdm *fdm, int fd, int events, fdm_fd_handler_t cb, void *data)
|
|
{
|
|
#if defined(_DEBUG)
|
|
tll_foreach(fdm->fds, it) {
|
|
if (it->item->fd == fd) {
|
|
BUG("FD=%d already registered", fd);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
struct fd_handler *handler = malloc(sizeof(*handler));
|
|
if (unlikely(handler == NULL)) {
|
|
LOG_ERRNO("malloc() failed");
|
|
return false;
|
|
}
|
|
|
|
*handler = (struct fd_handler) {
|
|
.fd = fd,
|
|
.events = events,
|
|
.callback = cb,
|
|
.callback_data = data,
|
|
.deleted = false,
|
|
};
|
|
|
|
tll_push_back(fdm->fds, handler);
|
|
|
|
struct epoll_event ev = {
|
|
.events = events,
|
|
.data = {.ptr = handler},
|
|
};
|
|
|
|
if (epoll_ctl(fdm->epoll_fd, EPOLL_CTL_ADD, fd, &ev) < 0) {
|
|
LOG_ERRNO("failed to register FD=%d with epoll", fd);
|
|
free(handler);
|
|
tll_pop_back(fdm->fds);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
fdm_del_internal(struct fdm *fdm, int fd, bool close_fd)
|
|
{
|
|
if (fd == -1)
|
|
return true;
|
|
|
|
tll_foreach(fdm->fds, it) {
|
|
if (it->item->fd != fd)
|
|
continue;
|
|
|
|
if (epoll_ctl(fdm->epoll_fd, EPOLL_CTL_DEL, fd, NULL) < 0)
|
|
LOG_ERRNO("failed to unregister FD=%d from epoll", fd);
|
|
|
|
if (close_fd)
|
|
close(it->item->fd);
|
|
|
|
it->item->deleted = true;
|
|
if (fdm->is_polling)
|
|
tll_push_back(fdm->deferred_delete, it->item);
|
|
else
|
|
free(it->item);
|
|
|
|
tll_remove(fdm->fds, it);
|
|
return true;
|
|
}
|
|
|
|
LOG_ERR("no such FD: %d", fd);
|
|
close(fd);
|
|
return false;
|
|
}
|
|
|
|
bool
|
|
fdm_del(struct fdm *fdm, int fd)
|
|
{
|
|
return fdm_del_internal(fdm, fd, true);
|
|
}
|
|
|
|
bool
|
|
fdm_del_no_close(struct fdm *fdm, int fd)
|
|
{
|
|
return fdm_del_internal(fdm, fd, false);
|
|
}
|
|
|
|
static bool
|
|
event_modify(struct fdm *fdm, struct fd_handler *fd, int new_events)
|
|
{
|
|
if (new_events == fd->events)
|
|
return true;
|
|
|
|
struct epoll_event ev = {
|
|
.events = new_events,
|
|
.data = {.ptr = fd},
|
|
};
|
|
|
|
if (epoll_ctl(fdm->epoll_fd, EPOLL_CTL_MOD, fd->fd, &ev) < 0) {
|
|
LOG_ERRNO("failed to modify FD=%d with epoll (events 0x%08x -> 0x%08x)",
|
|
fd->fd, fd->events, new_events);
|
|
return false;
|
|
}
|
|
|
|
fd->events = new_events;
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
fdm_event_add(struct fdm *fdm, int fd, int events)
|
|
{
|
|
tll_foreach(fdm->fds, it) {
|
|
if (it->item->fd != fd)
|
|
continue;
|
|
|
|
return event_modify(fdm, it->item, it->item->events | events);
|
|
}
|
|
|
|
LOG_ERR("FD=%d not registered with the FDM", fd);
|
|
return false;
|
|
}
|
|
|
|
bool
|
|
fdm_event_del(struct fdm *fdm, int fd, int events)
|
|
{
|
|
tll_foreach(fdm->fds, it) {
|
|
if (it->item->fd != fd)
|
|
continue;
|
|
|
|
return event_modify(fdm, it->item, it->item->events & ~events);
|
|
}
|
|
|
|
LOG_ERR("FD=%d not registered with the FDM", fd);
|
|
return false;
|
|
}
|
|
|
|
static hooks_t *
|
|
hook_priority_to_list(struct fdm *fdm, enum fdm_hook_priority priority)
|
|
{
|
|
switch (priority) {
|
|
case FDM_HOOK_PRIORITY_LOW: return &fdm->hooks_low;
|
|
case FDM_HOOK_PRIORITY_NORMAL: return &fdm->hooks_normal;
|
|
case FDM_HOOK_PRIORITY_HIGH: return &fdm->hooks_high;
|
|
}
|
|
|
|
BUG("unhandled priority type");
|
|
return NULL;
|
|
}
|
|
|
|
bool
|
|
fdm_hook_add(struct fdm *fdm, fdm_hook_t hook, void *data,
|
|
enum fdm_hook_priority priority)
|
|
{
|
|
hooks_t *hooks = hook_priority_to_list(fdm, priority);
|
|
|
|
#if defined(_DEBUG)
|
|
tll_foreach(*hooks, it) {
|
|
if (it->item.callback == hook) {
|
|
LOG_ERR("hook=0x%" PRIxPTR " already registered", (uintptr_t)hook);
|
|
return false;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
tll_push_back(*hooks, ((struct hook){hook, data}));
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
fdm_hook_del(struct fdm *fdm, fdm_hook_t hook, enum fdm_hook_priority priority)
|
|
{
|
|
hooks_t *hooks = hook_priority_to_list(fdm, priority);
|
|
|
|
tll_foreach(*hooks, it) {
|
|
if (it->item.callback != hook)
|
|
continue;
|
|
|
|
tll_remove(*hooks, it);
|
|
return true;
|
|
}
|
|
|
|
LOG_WARN("hook=0x%" PRIxPTR " not registered", (uintptr_t)hook);
|
|
return false;
|
|
}
|
|
|
|
static void
|
|
signal_handler(int signo)
|
|
{
|
|
received_signals[signo] = true;
|
|
}
|
|
|
|
bool
|
|
fdm_signal_add(struct fdm *fdm, int signo, fdm_signal_handler_t handler, void *data)
|
|
{
|
|
if (fdm->signal_handlers[signo].callback != NULL) {
|
|
LOG_ERR("signal %d already has a handler", signo);
|
|
return false;
|
|
}
|
|
|
|
sigset_t mask, original;
|
|
sigemptyset(&mask);
|
|
sigaddset(&mask, signo);
|
|
|
|
if (sigprocmask(SIG_BLOCK, &mask, &original) < 0) {
|
|
LOG_ERRNO("failed to block signal %d", signo);
|
|
return false;
|
|
}
|
|
|
|
struct sigaction action = {.sa_handler = &signal_handler};
|
|
if (sigaction(signo, &action, NULL) < 0) {
|
|
LOG_ERRNO("failed to set signal handler for signal %d", signo);
|
|
sigprocmask(SIG_SETMASK, &original, NULL);
|
|
return false;
|
|
}
|
|
|
|
received_signals[signo] = false;
|
|
fdm->signal_handlers[signo].callback = handler;
|
|
fdm->signal_handlers[signo].callback_data = data;
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
fdm_signal_del(struct fdm *fdm, int signo)
|
|
{
|
|
if (fdm->signal_handlers[signo].callback == NULL)
|
|
return false;
|
|
|
|
struct sigaction action = {.sa_handler = SIG_DFL};
|
|
if (sigaction(signo, &action, NULL) < 0) {
|
|
LOG_ERRNO("failed to restore signal handler for signal %d", signo);
|
|
return false;
|
|
}
|
|
|
|
received_signals[signo] = false;
|
|
fdm->signal_handlers[signo].callback = NULL;
|
|
fdm->signal_handlers[signo].callback_data = NULL;
|
|
|
|
sigset_t mask;
|
|
sigemptyset(&mask);
|
|
sigaddset(&mask, signo);
|
|
if (sigprocmask(SIG_UNBLOCK, &mask, NULL) < 0) {
|
|
LOG_ERRNO("failed to unblock signal %d", signo);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
fdm_poll(struct fdm *fdm)
|
|
{
|
|
xassert(!fdm->is_polling && "nested calls to fdm_poll() not allowed");
|
|
if (fdm->is_polling) {
|
|
LOG_ERR("nested calls to fdm_poll() not allowed");
|
|
return false;
|
|
}
|
|
|
|
tll_foreach(fdm->hooks_high, it) {
|
|
LOG_DBG(
|
|
"executing high priority hook 0x%" PRIxPTR" (fdm=%p, data=%p)",
|
|
(uintptr_t)it->item.callback, (void *)fdm,
|
|
(void *)it->item.callback_data);
|
|
it->item.callback(fdm, it->item.callback_data);
|
|
}
|
|
tll_foreach(fdm->hooks_normal, it) {
|
|
LOG_DBG(
|
|
"executing normal priority hook 0x%" PRIxPTR " (fdm=%p, data=%p)",
|
|
(uintptr_t)it->item.callback, (void *)fdm,
|
|
(void *)it->item.callback_data);
|
|
it->item.callback(fdm, it->item.callback_data);
|
|
}
|
|
tll_foreach(fdm->hooks_low, it) {
|
|
LOG_DBG(
|
|
"executing low priority hook 0x%" PRIxPTR " (fdm=%p, data=%p)",
|
|
(uintptr_t)it->item.callback, (void *)fdm,
|
|
(void *)it->item.callback_data);
|
|
it->item.callback(fdm, it->item.callback_data);
|
|
}
|
|
|
|
struct epoll_event events[tll_length(fdm->fds)];
|
|
|
|
int r = epoll_pwait(
|
|
fdm->epoll_fd, events, tll_length(fdm->fds), -1, &fdm->sigmask);
|
|
|
|
if (unlikely(r < 0)) {
|
|
if (errno == EINTR) {
|
|
/* TODO: is it possible to receive a signal without
|
|
* getting EINTR here? */
|
|
|
|
for (int i = 0; i < SIGRTMAX; i++) {
|
|
if (received_signals[i]) {
|
|
|
|
received_signals[i] = false;
|
|
struct sig_handler *handler = &fdm->signal_handlers[i];
|
|
|
|
xassert(handler->callback != NULL);
|
|
if (!handler->callback(fdm, i, handler->callback_data))
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
LOG_ERRNO("failed to epoll");
|
|
return false;
|
|
}
|
|
|
|
bool ret = true;
|
|
|
|
fdm->is_polling = true;
|
|
for (int i = 0; i < r; i++) {
|
|
struct fd_handler *fd = events[i].data.ptr;
|
|
if (fd->deleted)
|
|
continue;
|
|
|
|
if (!fd->callback(fdm, fd->fd, events[i].events, fd->callback_data)) {
|
|
ret = false;
|
|
break;
|
|
}
|
|
}
|
|
fdm->is_polling = false;
|
|
|
|
tll_foreach(fdm->deferred_delete, it) {
|
|
free(it->item);
|
|
tll_remove(fdm->deferred_delete, it);
|
|
}
|
|
|
|
return ret;
|
|
}
|