/*
 * Copyright (c) 2009 Mark Heily <mark@heily.com>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */
#include <signal.h>

#include "private.h"

/** Global control for whether we lock the kq_mtx when looking up kqueues
 */
bool libkqueue_thread_safe = true;

/** Global control for whether we perform cleanups on fork
 */
bool libkqueue_fork_cleanup = true;

/** Value is updated on fork to ensure all fork handlers are synchronised
 */
bool libkqueue_fork_cleanup_active;

/** Whether this is a child of the original process
 */
static bool libkqueue_in_child = false;

#ifdef _WIN32
/*
 * CRITICAL_SECTION can't be statically initialised; runtime init
 * happens in windows_libkqueue_init.
 */
tracing_mutex_t kq_mtx;
#else
tracing_mutex_t kq_mtx = TRACING_MUTEX_INITIALIZER;
#endif
pthread_once_t kq_is_initialized = PTHREAD_ONCE_INIT;

/** List of all active kqueues
 *
 * This is used to iterate over all active kqueues.
 *
 * kq_mtx should be held whilst accessing or modifying.
 */
struct kqueue_head kq_list;

/** Count of the active kqueues
 *
 */
unsigned int kq_cnt = 0;

unsigned int
get_fd_limit(void)
{
#ifdef _WIN32
    /* actually windows should be able to hold
       way more, as they use HANDLEs for everything.
       Still this number should still be sufficient for
       the provided number of kqueue fds.
       */
    return 65536;
#else
    struct rlimit rlim;

    if (getrlimit(RLIMIT_NOFILE, &rlim) < 0) {
        dbg_perror("getrlimit(2)");
        return (65536);
    } else {
        /*
         * macOS reports RLIMIT_NOFILE.rlim_max as RLIM_INFINITY by
         * default, which truncates to UINT_MAX and would have us
         * mmap'ing tens of gigabytes for the kqmap.  Cap at a
         * value that's plenty for any sane process.
         */
        rlim_t cap = 1U << 20;
        if (rlim.rlim_max == RLIM_INFINITY || rlim.rlim_max > cap)
            return (unsigned int) cap;
        return (rlim.rlim_max);
    }
#endif
}

unsigned int
get_fd_used(void)
{
#ifdef __linux__
    unsigned int fd_max = get_fd_limit();
    unsigned int i;
    unsigned int used = 0;
    int our_errno = errno; /* Preserve errno */

    for (i = 0; i < fd_max; i++) {
        if (fcntl(i, F_GETFD) == 0)
            used++;
    }

    errno = our_errno;

    return used;
#else
    return 0;
#endif
}

static struct map *kqmap;

void
libkqueue_free(void)
{
    /*
     * The issue here is that we're not sure
     */
    if (libkqueue_in_child) {
        dbg_puts("not releasing library resources as we are a child");
        return;
    }

    dbg_puts("releasing library resources");

    filter_free_all();

    if (kqops.libkqueue_free)
        kqops.libkqueue_free();
}

#ifndef _WIN32
/** TSAN incorrectly detects data races in this function
 *
 * It's not clear why it's not recording the fact that the appropriate
 * mutexes are in fact locked... but it is:
 *
 * Write of size 4 at 0x7f2ca355e34c by thread T1 (mutexes: write M4):
 *   #0 monitoring_thread_loop /home/arr2036/Documents/Repositories/dependencies/libkqueue/src/linux/platform.c:298 (libkqueue.so.0+0xf521)
 *
 * Previous write of size 4 at 0x7f2ca355e34c by main thread:
 *   #0 libkqueue_pre_fork /home/arr2036/Documents/Repositories/dependencies/libkqueue/src/common/kqueue.c:116 (libkqueue.so.0+0xbb8c)
 *   #1 <null> <null> (libc.so.6+0x94aff)
 *   #2 test_fork /home/arr2036/Documents/Repositories/dependencies/libkqueue/test/kqueue.c:168 (libkqueue-test+0x63cf)
 *   #3 run_iteration /home/arr2036/Documents/Repositories/dependencies/libkqueue/test/main.c:82 (libkqueue-test+0x7ca3)
 *   #4 test_harness /home/arr2036/Documents/Repositories/dependencies/libkqueue/test/main.c:109 (libkqueue-test+0x7e40)
 *   #5 main /home/arr2036/Documents/Repositories/dependencies/libkqueue/test/main.c:286 (libkqueue-test+0x8889)
 */
TSAN_IGNORE
void
libkqueue_pre_fork(void)
{
    /*
     * Unfortunately there's no way to remove the atfork
     * handlers, so all we can do if cleanup is
     * deactivated is bail as quickly as possible.
     *
     * We copy the value of libkqueue_fork_cleanup so
     * that it's consistent during the fork.
     */
    libkqueue_fork_cleanup_active = libkqueue_fork_cleanup;
    if (!libkqueue_fork_cleanup_active)
        return;

    /*
     * Ensure that all global structures are in a consistent
     * state before attempting the fork.
     *
     * If we don't do this then kq_mtx state will
     * be undefined when the child starts cleaning
     * up resources, and we could get deadlocks, nasty
     * memory corruption and/or crashes in the child.
     *
     * We always lock the kq_mtx even when
     * !libkqueue_thread_safe because it's still locked when
     * kqueues are being freed, and we don't want to access
     * freed memory when attempting to perform cleanup.
     */
    tracing_mutex_lock(&kq_mtx);

    /*
     * We previously attempted to lock all the child
     * kqueues, but when they were waiting in kevent_wait
     * this caused fork to stall.
     *
     * The main reason for locking the child kqueues was
     * to ensure filters and kqueues were in a consistent
     * state at the time of the fork, to ensure we could
     * free resources correctly... but as it was later
     * discovered many systems really don't like free()
     * being called in the forked child, so we were limited
     * to async-safe calls like close... These should be
     * safe no matter what the state of the kqueues so
     * the locking was removed.
     */
}

void
libkqueue_parent_fork(void)
{
    if (!libkqueue_fork_cleanup_active)
        return;

    dbg_puts("resuming execution in parent");

    tracing_mutex_unlock(&kq_mtx);
}

void
libkqueue_child_fork(void)
{
    libkqueue_in_child = true;

    if (!libkqueue_fork_cleanup_active)
        return;

    dbg_puts("cleaning up forked resources");
    filter_fork_all();

    if (kqops.libkqueue_fork)
        kqops.libkqueue_fork();

    tracing_mutex_unlock(&kq_mtx);
}
#endif

void
libkqueue_init(void)
{
   /*
    * Per-backend setup runs first so any platform-specific state
    * (e.g. Win32 Winsock, the global kq_mtx CRITICAL_SECTION, MSVC
    * Debug CRT heap surveillance) is in place before common init
    * touches it.
    */
   if (kqops.libkqueue_init)
       kqops.libkqueue_init();

#ifndef NDEBUG
    char *s = getenv("KQUEUE_DEBUG");
    if ((s != NULL) && (strlen(s) > 0) && (*s != '0')) {
        libkqueue_debug = 1;
        libkqueue_debug_func_init();
    }
#endif

   kqmap = map_new(get_fd_limit()); // INT_MAX
   if (kqmap == NULL)
       abort();

   filter_init_all();

   dbg_puts("library initialization complete");

#ifndef _WIN32
   pthread_atfork(libkqueue_pre_fork, libkqueue_parent_fork, libkqueue_child_fork);
#endif

#ifndef NDEBUG
   atexit(libkqueue_debug_ident_clear);
#endif
   atexit(libkqueue_free);
}

void
kqueue_knote_mark_disabled_all(struct kqueue *kq)
{
    unsigned int i;

    for (i = 0; i < EVFILT_SYSCOUNT; i++) {
        struct filter *kf = &kq->kq_filt[i];
        knote_mark_disabled_all(kf);
    }
}

/** Free a kqueue, must be called with the kq_mtx held
 *
 * On platforms that drop kq_lock across the kevent_wait syscall
 * (KEVENT_WAIT_DROP_LOCK), the user (or atexit/monitoring-thread
 * cleanup) may close the kqueue while another thread is blocked
 * inside the wait.  We can detect that case here: the per-kq
 * mutex is droppable, but kq_inflight (only non-empty under
 * KEVENT_WAIT_DROP_LOCK) is the authoritative "still in use"
 * indicator.  If non-empty we set kq_freeing and return without
 * destroying the kqueue.  The last in-flight caller out of
 * kevent() observes the flag and calls kqueue_complete_deferred_free
 * to finish the teardown.  Map removal and list unlinking happen
 * unconditionally so new callers see the kqueue as gone.
 */
void
kqueue_free(struct kqueue *kq)
{
    /*
     * Because this can be called during fork
     * processing the locker and unlocker may
     * be different.
     */
    tracing_mutex_assert_state(&kq_mtx, MTX_LOCKED);

    dbg_printf("kq=%p - freeing", kq);

    kq_cnt--;
    LIST_REMOVE(kq, kq_entry);

    /*
     * map_remove ensures the current map entry
     * points to this kqueue.
     *
     * If it doesn't we leave it alone and just
     * free the kq.
     */
    map_remove(kqmap, kq->kq_id, kq);

    /*
     * Ensure no other thread has any ongoing
     * operations on this kqueue.  Unlikely but
     * keeps TSAN Happy.
     */
    kqueue_lock(kq);

#ifdef KEVENT_WAIT_DROP_LOCK
    if (!TAILQ_EMPTY(&kq->kq_inflight)) {
        dbg_printf("kq=%p - in-flight callers exist, deferring teardown", kq);
        kq->kq_freeing = true;
        /*
         * Wake every parked waiter so they exit kevent() and the
         * deferred destruction can complete.  Without this, a
         * thread blocked in port_getn / epoll_wait with no timeout
         * stays parked forever and the kqueue leaks until process
         * exit.  Hook is optional; backends that don't (yet) have
         * a wake mechanism just rely on natural wakeups (events,
         * timeouts, signals).
         */
        if (kqops.kqueue_interrupt)
            kqops.kqueue_interrupt(kq);
        kqueue_unlock(kq);
        return;
    }
#endif

    filter_unregister_all(kq);
    kqops.kqueue_free(kq);
    kqueue_unlock(kq);

    tracing_mutex_destroy(&kq->kq_mtx);

#ifndef NDEBUG
    memset(kq, 0x42, sizeof(*kq));
#endif
    free(kq);
}

/** Complete a kqueue free that was deferred while in-flight callers existed.
 *
 * Called by the last kevent() caller out of kqueue_kevent_exit when it
 * observes kq_freeing == true.  At that point kq_inflight is empty and
 * the kqueue is unreachable via kqueue_lookup (kqueue_free already ran
 * map_remove + LIST_REMOVE), so this thread has exclusive ownership.
 *
 * Caller must NOT hold kq->kq_mtx (we re-acquire it briefly for the
 * filter teardown's assertions, then destroy the mutex).
 */
void
kqueue_complete_deferred_free(struct kqueue *kq)
{
    dbg_printf("kq=%p - completing deferred free", kq);

    kqueue_lock(kq);
    filter_unregister_all(kq);
    kqops.kqueue_free(kq);
    kqueue_unlock(kq);

    tracing_mutex_destroy(&kq->kq_mtx);

#ifndef NDEBUG
    memset(kq, 0x42, sizeof(*kq));
#endif
    free(kq);
}

void
kqueue_free_by_id(int id)
{
    struct kqueue *kq;

    kq = map_delete(kqmap, id);
    if (!kq) return;

    /*
     * With asynchronous close detection the stale kqueue is owned by
     * the backend's close-detection thread, which frees it when it
     * observes the close.  The map_delete above is all the eviction
     * needs; freeing it here too would double-free.
     */
    if (KQOPS_FLAG(KQUEUE_FLAG_CLOSE_ASYNC))
        return;

    kqueue_free(kq);
}

struct kqueue *
kqueue_lookup(int kq)
{
    return ((struct kqueue *) map_lookup(kqmap, kq));
}

int VISIBLE
kqueue(void)
{
    struct kqueue *kq;

    /*
     * pthread_once is the one-shot init barrier.  Concurrent
     * callers entering while another thread is still running
     * init_routine block until init returns; POSIX requires "On
     * return from pthread_once(), init_routine shall have
     * completed" with memory-ordering visibility, and the Win32
     * shim (pthread_once -> InitOnceExecuteOnce in
     * src/windows/platform.h) gives the same semantics natively.
     *
     * No wrapping kq_mtx: libkqueue_init calls pthread_atfork
     * which acquires libc's atfork-list lock, and fork() acquires
     * that same libc lock then runs our libkqueue_pre_fork which
     * takes kq_mtx - the inverse order.  Helgrind flagged it on
     * every proc test fork.
     */
    (void) pthread_once(&kq_is_initialized, libkqueue_init);

#ifndef _WIN32
    int prev_cancel_state;
    pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &prev_cancel_state);
#endif
    kq = calloc(1, sizeof(*kq));
    if (kq == NULL)
        return (-1);

    tracing_mutex_init(&kq->kq_mtx, NULL);

    /*
     * Init, evict and insert must be atomic under kq_mtx.  When an fd
     * number is reused for a new kqueue this serialises against the
     * Linux monitoring thread (which also takes kq_mtx to reap closed
     * kqueues), so neither side observes a half-updated kqmap/kq_list.
     */
    tracing_mutex_lock(&kq_mtx);
    if (kqops.kqueue_init(kq) < 0) {
        tracing_mutex_unlock(&kq_mtx);
    error:
        dbg_printf("kq=%p - init failed", kq);
        tracing_mutex_destroy(&kq->kq_mtx);
        free(kq);
#ifndef _WIN32
        pthread_setcancelstate(prev_cancel_state, NULL);
#endif
        return (-1);
    }

    dbg_printf("kq=%p - alloced with fd=%d", kq, kq->kq_id);

    /*
     * Evict any stale kqueue still mapped to this fd id (the previous
     * owner of this fd number closed it but hasn't been reaped yet).
     * kqueue_free_by_id drops it from the map and, unless close
     * detection is asynchronous, frees it.
     */
    kqueue_free_by_id(kq->kq_id);

    if (map_insert(kqmap, kq->kq_id, kq) < 0) {
        dbg_printf("kq=%p - map insertion failed, freeing", kq);
        /*
         * Per-kq lock is required by knote_delete_all's assertion;
         * the kq is unreachable from any other thread but the assert
         * is purely lockset-based, so satisfy it.
         */
        kqueue_lock(kq);
        filter_unregister_all(kq);
        kqueue_unlock(kq);
        tracing_mutex_unlock(&kq_mtx);
        goto error;
    }
    LIST_INSERT_HEAD(&kq_list, kq, kq_entry);
    kq_cnt++;
    tracing_mutex_unlock(&kq_mtx);
#ifndef _WIN32
    pthread_setcancelstate(prev_cancel_state, NULL);
    if (prev_cancel_state == PTHREAD_CANCEL_ENABLE)
        pthread_testcancel();
#endif

    return (kq->kq_id);
}
