/*
 * BSD kqueue() based ioloop notify handler.
 *
 * Copyright (c) 2005 Vaclav Haisman <v.haisman@sh.cvut.cz>
 */

#define _GNU_SOURCE
#include "lib.h"

#ifdef IOLOOP_NOTIFY_KQUEUE

#include "ioloop-private.h"
#include "llist.h"
#include "fd-close-on-exec.h"
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/event.h>
#include <sys/time.h>
#include <sys/stat.h>

/* kevent.udata's type just has to be different in NetBSD than in
   FreeBSD and OpenBSD.. */
#ifdef __NetBSD__
#  define MY_EV_SET(a, b, c, d, e, f, g) \
	EV_SET(a, b, c, d, e, f, (intptr_t)g)
#else
#  define MY_EV_SET(a, b, c, d, e, f, g) \
	EV_SET(a, b, c, d, e, f, g)
#endif

struct io_notify {
	struct io io;
	int refcount;
	int fd;
	struct io_notify *prev, *next;
};

struct ioloop_notify_handler_context {
	int kq;
	struct io *event_io;
	struct io_notify *notifies;
};

static void
io_loop_notify_free(struct ioloop_notify_handler_context *ctx,
		    struct io_notify *io)
{
	DLLIST_REMOVE(&ctx->notifies, io);
	i_free(io);
}

static void event_callback(struct ioloop_notify_handler_context *ctx)
{
	struct io_notify *io;
	struct kevent events[64];
	struct timespec ts;
	int i, ret;

	ts.tv_sec = 0;
	ts.tv_nsec = 0;

	ret = kevent(ctx->kq, NULL, 0, events, N_ELEMENTS(events), &ts);
	if (ret <= 0) {
		if (ret == 0 || errno == EINTR)
			return;

		i_fatal("kevent(notify) failed: %m");
	}

	if (gettimeofday(&ioloop_timeval, NULL) < 0)
		i_fatal("gettimeofday() failed: %m");
	ioloop_time = ioloop_timeval.tv_sec;

	for (i = 0; i < ret; i++) {
		io = (void *)events[i].udata;
		i_assert(io->refcount >= 1);
		io->refcount++;
	}
	for (i = 0; i < ret; i++) {
		io = (void *)events[i].udata;
		/* there can be multiple events for a single io.
		   call the callback only once if that happens. */
		if (io->refcount == 2 && io->io.callback != NULL)
			io_loop_call_io(&io->io);

		if (--io->refcount == 0)
			io_loop_notify_free(ctx, io);
	}
}

static struct ioloop_notify_handler_context *io_loop_notify_handler_init(void)
{
	struct ioloop_notify_handler_context *ctx;

	ctx = current_ioloop->notify_handler_context =
		i_new(struct ioloop_notify_handler_context, 1);
	ctx->kq = kqueue();
	if (ctx->kq < 0)
		i_fatal("kqueue(notify) failed: %m");
	fd_close_on_exec(ctx->kq, TRUE);
	return ctx;
}

void io_loop_notify_handler_deinit(struct ioloop *ioloop)
{
	struct ioloop_notify_handler_context *ctx =
		ioloop->notify_handler_context;

	while (ctx->notifies != NULL) {
		struct io_notify *io = ctx->notifies;
		struct io *_io = &io->io;

		i_warning("I/O notify leak: %p (line %u, fd %d)",
			  (void *)_io->callback,
			  _io->source_linenum, io->fd);
		io_remove(&_io);
	}

	if (ctx->event_io)
		io_remove(&ctx->event_io);
	if (close(ctx->kq) < 0)
		i_error("close(kqueue notify) failed: %m");
	i_free(ctx);
}

#undef io_add_notify
enum io_notify_result
io_add_notify(const char *path, unsigned int source_linenum,
	      io_callback_t *callback, void *context, struct io **io_r)
{
	struct ioloop_notify_handler_context *ctx =
		current_ioloop->notify_handler_context;
	struct kevent ev;
	struct io_notify *io;
	int fd;

	if (ctx == NULL)
		ctx = io_loop_notify_handler_init();

	fd = open(path, O_RDONLY);
	if (fd == -1) {
		/* ESTALE could happen with NFS. Don't bother giving an error
		   message then. */
		if (errno != ENOENT && errno != ESTALE)
			i_error("open(%s) for kq notify failed: %m", path);
		return IO_NOTIFY_NOTFOUND;
	}
	fd_close_on_exec(fd, TRUE);

	io = i_new(struct io_notify, 1);
	io->io.condition = IO_NOTIFY;
	io->io.source_linenum = source_linenum;
	io->io.callback = callback;
	io->io.context = context;
	io->io.ioloop = current_ioloop;
	io->refcount = 1;
	io->fd = fd;

	/* EV_CLEAR flag is needed because the EVFILT_VNODE filter reports
	   event state transitions and not the current state.  With this flag,
	   the same event is only returned once. */
	MY_EV_SET(&ev, fd, EVFILT_VNODE, EV_ADD | EV_CLEAR,
		  NOTE_DELETE | NOTE_RENAME | NOTE_WRITE | NOTE_EXTEND |
		  NOTE_REVOKE, 0, io);
	if (kevent(ctx->kq, &ev, 1, NULL, 0, NULL) < 0) {
		i_error("kevent(%d, %s) for notify failed: %m", fd, path);
		i_close_fd(&fd);
		i_free(io);
		return IO_NOTIFY_NOSUPPORT;
	}

	if (ctx->event_io == NULL) {
		ctx->event_io = io_add(ctx->kq, IO_READ, event_callback,
				       io->io.ioloop->notify_handler_context);
	}
	DLLIST_PREPEND(&ctx->notifies, io);
	*io_r = &io->io;
	return IO_NOTIFY_ADDED;
}

void io_loop_notify_remove(struct io *_io)
{
	struct ioloop_notify_handler_context *ctx =
		_io->ioloop->notify_handler_context;
	struct io_notify *io = (struct io_notify *)_io;
	struct kevent ev;

	MY_EV_SET(&ev, io->fd, EVFILT_VNODE, EV_DELETE, 0, 0, NULL);
	if (kevent(ctx->kq, &ev, 1, NULL, 0, 0) < 0)
		i_error("kevent(%d) for notify remove failed: %m", io->fd);
	if (close(io->fd) < 0)
		i_error("close(%d) for notify remove failed: %m", io->fd);
	io->fd = -1;

	if (--io->refcount == 0)
		io_loop_notify_free(ctx, io);
}

int io_loop_extract_notify_fd(struct ioloop *ioloop)
{
	struct ioloop_notify_handler_context *ctx =
		ioloop->notify_handler_context;
	struct io_notify *io;
	int fd, new_kq;

	if (ctx->kq == -1)
		return -1;

	new_kq = kqueue();
	if (new_kq < 0) {
		i_error("kqueue(notify) failed: %m");
		return -1;
	}
	for (io = ctx->notifies; io != NULL; io = io->next)
		io->fd = -1;
	if (ctx->event_io != NULL)
		io_remove(&ctx->event_io);
	fd = ctx->kq;
	ctx->kq = new_kq;
	return fd;
}

#endif
