/* doscan - Denial Of Service Capable Auditing of Networks       -*- C++ -*-
 * Copyright (C) 2003 Florian Weimer
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#include "config.h"
#include "event_queue_epoll.h"
#include "opt.h"

#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>

#if defined(__linux__) && defined(HAVE_SYS_EPOLL_H)

#define HAVE_EPOLL

#include <sys/epoll.h>

#ifndef HAVE_EPOLL_WAIT

// Some GNU libc versions provide the correct header file, but no
// implementations.  Some even lack the syscall numbers.

extern "C" {

#include <asm/unistd.h>

#ifndef __NR_epoll_create
#define __NR_epoll_create       254
#define __NR_epoll_ctl          255
#define __NR_epoll_wait         256
#endif // __NR_epoll_create

_syscall1(int, epoll_create, int, size)
_syscall4(int, epoll_ctl, int, epfd, int, op, int, fd,
          struct epoll_event *, event)
_syscall4(int, epoll_wait, int, epfd, struct epoll_event *, pevents,
          int, maxevents, int, timeout)

}

#endif // HAVE_EPOLL_WAIT

// event_queue_epoll implementation

inline unsigned
event_queue_epoll::convert_watch(fd_handler::watch_options w)
{
  switch (w) {
    case fd_handler::watch_read:
      return EPOLLIN | EPOLLERR;
    case fd_handler::watch_write:
      return EPOLLOUT | EPOLLERR;
    case fd_handler::watch_read_write:
      return EPOLLIN | EPOLLOUT | EPOLLERR;
    default:
      abort();
  }
}

void
event_queue_epoll::forget_activity(event_queue::fd_handler* fdh)
{
  // Iterator from the end because we process activities from the end,
  // so it is likely the loop terminates early.
  for (fd_activities_t::reverse_iterator p = fd_activities.rbegin();
       p != fd_activities.rend(); ++p) {
    if (p->fdh == fdh) {
      fd_activities.erase(p.base() - 1);
      break;
    }
  }
}

void
event_queue_epoll::add_fd(fd_handler* fdh, fd_handler::watch_options w)
{
  epoll_event event;
  event.events = convert_watch(w);
  event.data.ptr = fdh;

  int result = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fdh->fd(), &event);
  if (result == -1) {
    fprintf(stderr, "%s: epoll_ctl(EPOLL_CTL_ADD) failed: %s\n",
            opt_program, strerror(errno));
    exit(1);
  }

  ++count;
}

void
event_queue_epoll::update_fd(fd_handler* fdh, fd_handler::watch_options w)
{
  epoll_event event;
  event.events = convert_watch(w);
  event.data.ptr = fdh;

  int result = epoll_ctl(epoll_fd, EPOLL_CTL_MOD, fdh->fd(), &event);
  if (result == -1) {
    fprintf(stderr, "%s: epoll_ctl(EPOLL_CTL_MOD) failed: %s\n",
            opt_program, strerror(errno));
    exit(1);
  }
}

void
event_queue_epoll::remove_fd(fd_handler* fdh)
{
  forget_activity(fdh);

  epoll_event event;
  event.events = 0;
  event.data.ptr = 0;

  int result = epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fdh->fd(), &event);
  if (result == -1) {
    fprintf(stderr, "%s: epoll_ctl(EPOLL_CTL_DEL) failed: %s\n",
            opt_program, strerror(errno));
    exit(1);
  }

  --count;
}

event_queue_epoll::event_queue_epoll(unsigned size_hint)
{
  int result = epoll_create(size_hint);
  if (result == -1) {
    fprintf(stderr, "%s: epoll_create failed: %s\n",
            opt_program, strerror(errno));
    exit(1);
  }

  epoll_fd = result;
  count = 0;
}

event_queue_epoll::~event_queue_epoll()
{
  close(epoll_fd);
}

void
event_queue_epoll::run()
{
  for (;;) {
    int timeout = next_timeout();
    if (count == 0) {
      if (timeout < 0) {
        return;                 // infinite timeout without any fds
      }
      usleep(1000 * timeout);
      dispatch_start();
      // No fd activity to dispatch.  Timeouts are handled below.
    } else {
      static unsigned const max_events = 32;
      struct epoll_event events[max_events];
      int result = epoll_wait(epoll_fd, events, sizeof(max_events), timeout);

      if (result < 0) {
        if (errno != EINTR) {
          fprintf(stderr, "%s: epoll_wait() failure, error was: %s\n",
                  opt_program, strerror(errno));
          exit (EXIT_FAILURE);
        }
      }

      dispatch_start();
      if (result > 0) {
        // We have to make a copy of the array so that it's not
        // corrupted because of remove_fd() calls.

        fd_activities.clear();
        for (int j = 0; j < result; ++j) {
          fd_activity fa;
          fa.fdh = static_cast<fd_handler*>(events[j].data.ptr);

          if (events[j].events & EPOLLERR) {
            fa.act = fd_handler::activity_error;
          } else if (events[j].events & EPOLLIN) {
            if (events[j].events & EPOLLOUT) {
              fa.act = fd_handler::activity_read_write;
            } else {
              fa.act = fd_handler::activity_read;
            }
          } else {
            if (events[j].events & EPOLLOUT) {
              fa.act = fd_handler::activity_write;
            } else {
              fprintf(stderr, "%s: warning: illegal epoll_wait() state 0x%08X "
                      "received on descriptor %d\n",
                      opt_program, events[j].events, fa.fdh->fd());
              fa.act = fd_handler::activity_error;
            }
          }
          fd_activities.push_back(fa);
        }

        while (!fd_activities.empty()) {
          fd_activity fa = *(fd_activities.end() - 1);

          if (!fa.fdh->on_activity(fa.act)) {
            forget_activity(fa.fdh);
            delete fa.fdh;
          } else {
            forget_activity(fa.fdh);
          }
        }
      }
    }
    dispatch_end();
  }
}

#else // __linux__

#undef HAVE_EPOLL

#endif //__linux__

// event_queue::create() implementation.  We dynamically switch
// between event_queue_poll and event_queue_epoll.

#include "event_queue_poll.h"

event_queue*
event_queue::create(unsigned size_hint)
{
#ifdef HAVE_EPOLL
  if (!opt_no_epoll) {
    static int have_epoll = -1;

    if (have_epoll < 0) {
      int result = epoll_create(1);
      if (result >= 0) {
        close(result);
        have_epoll = 1;
        if (opt_verbose) {
          fprintf(stderr, "%s: using epoll interface\n", opt_program);
        }
      } else {
        have_epoll = 0;
        if (opt_verbose) {
          fprintf(stderr, "%s: using poll interface\n", opt_program);
        }
      }
    }
    if (have_epoll > 0) {
      return new event_queue_epoll(size_hint);
    }
  }
#endif

  return new event_queue_poll();
}
