/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
 * epoll(7) file descriptor monitoring
 */

#include "qemu/osdep.h"
#include <sys/epoll.h>
#include "qemu/rcu_queue.h"
#include "aio-posix.h"

/* The fd number threshold to switch to epoll */
#define EPOLL_ENABLE_THRESHOLD 64

void fdmon_epoll_disable(AioContext *ctx)
{
    if (ctx->epollfd >= 0) {
        close(ctx->epollfd);
        ctx->epollfd = -1;
    }

    /* Switch back */
    ctx->fdmon_ops = &fdmon_poll_ops;
}

static inline int epoll_events_from_pfd(int pfd_events)
{
    return (pfd_events & G_IO_IN ? EPOLLIN : 0) |
           (pfd_events & G_IO_OUT ? EPOLLOUT : 0) |
           (pfd_events & G_IO_HUP ? EPOLLHUP : 0) |
           (pfd_events & G_IO_ERR ? EPOLLERR : 0);
}

static void fdmon_epoll_update(AioContext *ctx,
                               AioHandler *old_node,
                               AioHandler *new_node)
{
    struct epoll_event event = {
        .data.ptr = new_node,
        .events = new_node ? epoll_events_from_pfd(new_node->pfd.events) : 0,
    };
    int r;

    if (!new_node) {
        r = epoll_ctl(ctx->epollfd, EPOLL_CTL_DEL, old_node->pfd.fd, &event);
    } else if (!old_node) {
        r = epoll_ctl(ctx->epollfd, EPOLL_CTL_ADD, new_node->pfd.fd, &event);
    } else {
        r = epoll_ctl(ctx->epollfd, EPOLL_CTL_MOD, new_node->pfd.fd, &event);
    }

    if (r) {
        fdmon_epoll_disable(ctx);
    }
}

static int fdmon_epoll_wait(AioContext *ctx, AioHandlerList *ready_list,
                            int64_t timeout)
{
    GPollFD pfd = {
        .fd = ctx->epollfd,
        .events = G_IO_IN | G_IO_OUT | G_IO_HUP | G_IO_ERR,
    };
    AioHandler *node;
    int i, ret = 0;
    struct epoll_event events[128];

    if (timeout > 0) {
        ret = qemu_poll_ns(&pfd, 1, timeout);
        if (ret > 0) {
            timeout = 0;
        }
    }
    if (timeout <= 0 || ret > 0) {
        ret = epoll_wait(ctx->epollfd, events,
                         ARRAY_SIZE(events),
                         timeout);
        if (ret <= 0) {
            goto out;
        }
        for (i = 0; i < ret; i++) {
            int ev = events[i].events;
            int revents = (ev & EPOLLIN ? G_IO_IN : 0) |
                          (ev & EPOLLOUT ? G_IO_OUT : 0) |
                          (ev & EPOLLHUP ? G_IO_HUP : 0) |
                          (ev & EPOLLERR ? G_IO_ERR : 0);

            node = events[i].data.ptr;
            aio_add_ready_handler(ready_list, node, revents);
        }
    }
out:
    return ret;
}

static const FDMonOps fdmon_epoll_ops = {
    .update = fdmon_epoll_update,
    .wait = fdmon_epoll_wait,
    .need_wait = aio_poll_disabled,
};

static bool fdmon_epoll_try_enable(AioContext *ctx)
{
    AioHandler *node;
    struct epoll_event event;

    QLIST_FOREACH_RCU(node, &ctx->aio_handlers, node) {
        int r;
        if (QLIST_IS_INSERTED(node, node_deleted) || !node->pfd.events) {
            continue;
        }
        event.events = epoll_events_from_pfd(node->pfd.events);
        event.data.ptr = node;
        r = epoll_ctl(ctx->epollfd, EPOLL_CTL_ADD, node->pfd.fd, &event);
        if (r) {
            return false;
        }
    }

    ctx->fdmon_ops = &fdmon_epoll_ops;
    return true;
}

bool fdmon_epoll_try_upgrade(AioContext *ctx, unsigned npfd)
{
    bool ok;

    if (ctx->epollfd < 0) {
        return false;
    }

    if (npfd < EPOLL_ENABLE_THRESHOLD) {
        return false;
    }

    /* The list must not change while we add fds to epoll */
    if (!qemu_lockcnt_dec_if_lock(&ctx->list_lock)) {
        return false;
    }

    ok = fdmon_epoll_try_enable(ctx);

    qemu_lockcnt_inc_and_unlock(&ctx->list_lock);

    if (!ok) {
        fdmon_epoll_disable(ctx);
    }
    return ok;
}

void fdmon_epoll_setup(AioContext *ctx)
{
    ctx->epollfd = epoll_create1(EPOLL_CLOEXEC);
    if (ctx->epollfd == -1) {
        fprintf(stderr, "Failed to create epoll instance: %s", strerror(errno));
    }
}
