/*
 * ovdb_server.c
 * ovdb read server
 */

#include "config.h"
#include "clibrary.h"
#include "portable/mmap.h"
#include "portable/time.h"
#include "portable/setproctitle.h"
#include "portable/socket.h"
#include "portable/wait.h"
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#ifdef HAVE_SYS_SELECT_H
# include <sys/select.h>
#endif
#include <syslog.h>

#ifdef HAVE_UNIX_DOMAIN_SOCKETS
# include <sys/un.h>
#endif

#include "inn/innconf.h"
#include "inn/messages.h"
#include "libinn.h"
#include "paths.h"
#include "storage.h"
#include "ov.h"

#include "../storage/ovdb/ovdb.h"
#include "../storage/ovdb/ovdb-private.h"

#ifndef USE_BERKELEY_DB

int
main(int argc UNUSED, char **argv UNUSED)
{
    die("BerkeleyDB support not compiled");
}

#else /* USE_BERKELEY_DB */


#define SELECT_TIMEOUT 15


/* This will work unless user sets a larger clienttimeout
   in readers.conf */
#define CLIENT_TIMEOUT (innconf->clienttimeout + 60)
/*#define CLIENT_TIMEOUT 3600*/


static int listensock;

#define MODE_READ   0
#define MODE_WRITE  1
#define MODE_CLOSED 2
#define STATE_READCMD 0
#define STATE_READGROUP 1
struct reader {
    int fd;
    int mode;
    int state;
    int buflen;
    int bufpos;
    void *buf;
    time_t lastactive;
    void *currentsearch;
};

static struct reader *readertab;
static int readertablen;
static int numreaders;
static time_t now;
static pid_t parent;

struct child {
    pid_t pid;
    int num;
    time_t started;
};
static struct child *children;
#define wholistens (children[ovdb_conf.numrsprocs].num)

static int signalled = 0;
static void
sigfunc(int sig UNUSED)
{
    signalled = 1;
}

static int updated = 0;
static void
childsig(int sig UNUSED)
{
    updated = 1;
}

static void
parentsig(int sig UNUSED)
{
    int i, which, smallest;
    if(wholistens < 0) {
	which = smallest = -1;
	for(i = 0; i < ovdb_conf.numrsprocs; i++) {
	    if(children[i].pid == -1)
		continue;
	    if(!ovdb_conf.maxrsconn || children[i].num <= ovdb_conf.maxrsconn) {
		if(smallest == -1 || children[i].num < smallest) {
		    smallest = children[i].num;
		    which = i;
		}
	    }
	}
	if(which != -1) {
	    wholistens = which;
	    kill(children[which].pid, SIGUSR1);
	} else {
	    wholistens = -2;
	}
	updated = 1;
    }
}

static int putpid(const char *path)
{
    char buf[30];
    int fd = open(path, O_WRONLY|O_TRUNC|O_CREAT, 0664);
    if(fd == -1) {
        syswarn("cannot open %s", path);
        return -1;
    }
    snprintf(buf, sizeof(buf), "%d\n", getpid());
    if(write(fd, buf, strlen(buf)) < 0) {
        syswarn("cannot write to %s", path);
        close(fd);
        return -1;
    }
    close(fd);
    return 0;
}

static void
do_groupstats(struct reader *r)
{
    struct rs_groupstats *reply;
    char *group = (char *)(r->buf) + sizeof(struct rs_cmd);
    reply = xmalloc(sizeof(struct rs_groupstats));

    /*syslog(LOG_DEBUG, "OVDB: rs: do_groupstats '%s'", group);*/
    if(ovdb_groupstats(group, &reply->lo, &reply->hi, &reply->count, &reply->flag)) {
	reply->status = CMD_GROUPSTATS;
    	reply->aliaslen = 0;
    } else {
	reply->status = CMD_GROUPSTATS | RPLY_ERROR;
    }
    free(r->buf);
    r->buf = reply;
    r->buflen = sizeof(struct rs_groupstats);
    r->bufpos = 0;
    r->mode = MODE_WRITE;
}

static void
do_opensrch(struct reader *r)
{
    struct rs_cmd *cmd = r->buf;
    struct rs_opensrch *reply;
    char *group = (char *)(r->buf) + sizeof(struct rs_cmd);
    reply = xmalloc(sizeof(struct rs_opensrch));

    /*syslog(LOG_DEBUG, "OVDB: rs: do_opensrch '%s' %d %d", group, cmd->artlo, cmd->arthi);*/

    if(r->currentsearch != NULL) {
	/* can only open one search at a time */
	reply->status = CMD_OPENSRCH | RPLY_ERROR;
    } else {
	reply->handle = ovdb_opensearch(group, cmd->artlo, cmd->arthi);
	if(reply->handle == NULL) {
	    reply->status = CMD_OPENSRCH | RPLY_ERROR;
	} else {
	    reply->status = CMD_OPENSRCH;
	}
	r->currentsearch = reply->handle;
    }
    free(r->buf);
    r->buf = reply;
    r->buflen = sizeof(struct rs_opensrch);
    r->bufpos = 0;
    r->mode = MODE_WRITE;
}

static void
do_srch(struct reader *r)
{
    struct rs_cmd *cmd = r->buf;
    struct rs_srch *reply;
    ARTNUM artnum;
    TOKEN token;
    time_t arrived;
    int len;
    char *data;

    if(ovdb_search(cmd->handle, &artnum, &data, &len, &token, &arrived)) {
    	reply = xmalloc(sizeof(struct rs_srch) + len);
	reply->status = CMD_SRCH;
	reply->artnum = artnum;
	reply->token = token;
	reply->arrived = arrived;
	reply->len = len;
	memcpy((char *)reply + sizeof(struct rs_srch), data, len);
	r->buflen = sizeof(struct rs_srch) + len;
    } else {
    	reply = xmalloc(sizeof(struct rs_srch));
	reply->status = CMD_SRCH | RPLY_ERROR;
	r->buflen = sizeof(struct rs_srch);
    }
    free(r->buf);
    r->buf = reply;
    r->bufpos = 0;
    r->mode = MODE_WRITE;
}

static void
do_closesrch(struct reader *r)
{
    struct rs_cmd *cmd = r->buf;

    ovdb_closesearch(cmd->handle);
    free(r->buf);
    r->buf = NULL;
    r->bufpos = r->buflen = 0;
    r->mode = MODE_READ;
    r->currentsearch = NULL;
}

static void
do_artinfo(struct reader *r)
{
    struct rs_cmd *cmd = r->buf;
    struct rs_artinfo *reply;
    char *group = (char *)(r->buf) + sizeof(struct rs_cmd);
    TOKEN token;

    /*syslog(LOG_DEBUG, "OVDB: rs: do_artinfo: '%s' %d", group, cmd->artlo);*/
    if(ovdb_getartinfo(group, cmd->artlo, &token)) {
    	reply = xmalloc(sizeof(struct rs_artinfo));
	reply->status = CMD_ARTINFO;
	reply->token = token;
	r->buflen = sizeof(struct rs_artinfo);
    } else {
    	reply = xmalloc(sizeof(struct rs_artinfo));
	reply->status = CMD_ARTINFO | RPLY_ERROR;
	r->buflen = sizeof(struct rs_artinfo);
    }
    free(r->buf);
    r->buf = reply;
    r->bufpos = 0;
    r->mode = MODE_WRITE;
}


static int
process_cmd(struct reader *r)
{
    struct rs_cmd *cmd = r->buf;

    if(r->state == STATE_READCMD) {
	switch(cmd->what) {
	case CMD_GROUPSTATS:
	case CMD_OPENSRCH:
	case CMD_ARTINFO:
	    r->state = STATE_READGROUP;
	    if(cmd->grouplen == 0) {
		/* shoudn't happen... */
		r->mode = MODE_CLOSED;
		close(r->fd);
		free(r->buf);
		r->buf = NULL;
		return 0;
	    }
	    r->buflen += cmd->grouplen;
            r->buf = xrealloc(r->buf, r->buflen);
	    return 1;
	}
    }

    switch(cmd->what) {
    case CMD_GROUPSTATS:
	((char *)r->buf)[r->buflen - 1] = 0;	/* make sure group is null-terminated */
	do_groupstats(r);
	break;
    case CMD_OPENSRCH:
	((char *)r->buf)[r->buflen - 1] = 0;
	do_opensrch(r);
	break;
    case CMD_SRCH:
	do_srch(r);
	break;
    case CMD_CLOSESRCH:
	do_closesrch(r);
	break;
    case CMD_ARTINFO:
	((char *)r->buf)[r->buflen - 1] = 0;
	do_artinfo(r);
	break;
    default:
	r->mode = MODE_CLOSED;
	close(r->fd);
	free(r->buf);
	r->buf = NULL;
	break;
    }

    return 0;
}

static void
handle_read(struct reader *r)
{
    int n;
    r->lastactive = now;

    if(r->buf == NULL) {
    	r->state = STATE_READCMD;
    	r->buf = xmalloc(sizeof(struct rs_cmd));
	r->buflen = sizeof(struct rs_cmd);
	r->bufpos = 0;
    }
again:
    n = read(r->fd, (char *)(r->buf) + r->bufpos, r->buflen - r->bufpos);
    if(n <= 0) {
	if(n < 0 && (errno == EAGAIN || errno == EINTR || errno == EWOULDBLOCK))
	    return;
    	r->mode = MODE_CLOSED;
	close(r->fd);
	free(r->buf);
	r->buf = NULL;
    }
    r->bufpos += n;

    if(r->bufpos >= r->buflen)
	if(process_cmd(r))
	    goto again;
}

static void
handle_write(struct reader *r)
{
    int n;
    r->lastactive = now;

    if(r->buf == NULL)	/* shouldn't happen */
	return;

    n = write(r->fd, (char *)(r->buf) + r->bufpos, r->buflen - r->bufpos);
    if(n <= 0) {
	if(n < 0 && (errno == EAGAIN || errno == EINTR || errno == EWOULDBLOCK))
	    return;
    	r->mode = MODE_CLOSED;
	close(r->fd);
	free(r->buf);
	r->buf = NULL;
    }
    r->bufpos += n;

    if(r->bufpos >= r->buflen) {
	free(r->buf);
    	r->buf = NULL;
	r->bufpos = r->buflen = 0;
	r->mode = MODE_READ;
    }
}

static void
newclient(int fd)
{
    struct reader *r;
    int i;

    nonblocking(fd, 1);

    if(numreaders >= readertablen) {
    	readertablen += 50;
        readertab = xrealloc(readertab, readertablen * sizeof(struct reader));
        for(i = numreaders; i < readertablen; i++) {
	    readertab[i].mode = MODE_CLOSED;
	    readertab[i].buf = NULL;
	}
    }

    r = &(readertab[numreaders]);
    numreaders++;

    r->fd = fd;
    r->mode = MODE_WRITE;
    r->buflen = sizeof(OVDB_SERVER_BANNER);
    r->bufpos = 0;
    r->buf = xstrdup(OVDB_SERVER_BANNER);
    r->lastactive = now;
    r->currentsearch = NULL;

    handle_write(r);
}

static void
delclient(int which)
{
    int i;
    struct reader *r = &(readertab[which]);

    if(r->mode != MODE_CLOSED)
    	close(r->fd);

    if(r->buf != NULL) {
    	free(r->buf);
    }
    if(r->currentsearch != NULL) {
	ovdb_closesearch(r->currentsearch);
	r->currentsearch = NULL;
    }

    /* numreaders will get decremented by the calling function */
    for(i = which; i < numreaders-1; i++)
    	readertab[i] = readertab[i+1];

    readertab[i].mode = MODE_CLOSED;
    readertab[i].buf = NULL;
}

static pid_t
serverproc(int me)
{
    fd_set rdset, wrset;
    int i, ret, count, lastfd, lastnumreaders;
    socklen_t salen;
    struct sockaddr_in sa;
    struct timeval tv;
    pid_t pid;

    pid = fork();
    if (pid != 0)
	return pid;

    if (!ovdb_open(OV_READ|OVDB_SERVER))
        die("cannot open overview");
    xsignal_norestart(SIGINT, sigfunc);
    xsignal_norestart(SIGTERM, sigfunc);
    xsignal_norestart(SIGHUP, sigfunc);
    xsignal_norestart(SIGUSR1, childsig);
    xsignal(SIGPIPE, SIG_IGN);

    numreaders = lastnumreaders = 0;
    if(ovdb_conf.maxrsconn) {
	readertablen = ovdb_conf.maxrsconn;
    } else {
    	readertablen = 50;
    }
    readertab = xmalloc(readertablen * sizeof(struct reader));
    for(i = 0; i < readertablen; i++) {
	readertab[i].mode = MODE_CLOSED;
	readertab[i].buf = NULL;
    }

    setproctitle("0 clients");

    /* main loop */
    while(!signalled) {
	FD_ZERO(&rdset);
	FD_ZERO(&wrset);
	lastfd = 0;
	if(wholistens == me) {
	    if(!ovdb_conf.maxrsconn || numreaders < ovdb_conf.maxrsconn) {
		FD_SET(listensock, &rdset);
		lastfd = listensock;
                setproctitle("%d client%s *", numreaders,
                             numreaders == 1 ? "" : "s");
	    } else {
		wholistens = -1;
		kill(parent, SIGUSR1);
	    }
        }

	for(i = 0; i < numreaders; i++) {
	    switch(readertab[i].mode) {
	    case MODE_READ:
	    	FD_SET(readertab[i].fd, &rdset);
		break;
	    case MODE_WRITE:
	    	FD_SET(readertab[i].fd, &wrset);
		break;
	    default:
	    	continue;
	    }
	    if(readertab[i].fd > lastfd)
	    	lastfd = readertab[i].fd;
	}
	tv.tv_usec = 0;
	tv.tv_sec = SELECT_TIMEOUT;
	count = select(lastfd + 1, &rdset, &wrset, NULL, &tv);

	if(signalled)
	    break;
	if(count <= 0)
	    continue;

	now = time(NULL);

	if(FD_ISSET(listensock, &rdset)) {
	    if(!ovdb_conf.maxrsconn || numreaders < ovdb_conf.maxrsconn) {
		salen = sizeof(sa);
	    	ret = accept(listensock, (struct sockaddr *)&sa, &salen);
		if(ret >= 0) {
		    newclient(ret);
		    wholistens = -1;
		    children[me].num = numreaders;
		    kill(parent, SIGUSR1);
		}
	    }
	}

	for(i = 0; i < numreaders; i++) {
	    switch(readertab[i].mode) {
	    case MODE_READ:
	    	if(FD_ISSET(readertab[i].fd, &rdset))
		    handle_read(&(readertab[i]));
	        break;
	    case MODE_WRITE:
	    	if(FD_ISSET(readertab[i].fd, &wrset))
		    handle_write(&(readertab[i]));
		break;
	    }
	}

	for(i = 0; i < numreaders; i++) {
	    if(readertab[i].mode == MODE_CLOSED
		  || readertab[i].lastactive + CLIENT_TIMEOUT < now) {
	    	delclient(i);
		numreaders--;
		i--;
	    }
	}
	if(children[me].num != numreaders) {
	    children[me].num = numreaders;
	    kill(parent, SIGUSR1);
        }
	if(numreaders != lastnumreaders) {
	    lastnumreaders = numreaders;
            setproctitle("%d client%s", numreaders,
                         numreaders == 1 ? "" : "s");
	}
    }

    ovdb_close();
    exit(0);
}

static int
reap(void)
{
    int i, cs;
    pid_t c;

    while((c = waitpid(-1, &cs, WNOHANG)) > 0) {
	for(i = 0; i < ovdb_conf.numrsprocs; i++) {
	    if(c == children[i].pid) {
		if(children[i].started + 30 > time(NULL))
		    return 1;

		children[i].num = 0;

		if(wholistens == i)
		    wholistens = -1;

		if((children[i].pid = serverproc(i)) == -1)
		    return 1;

		children[i].started = time(NULL);
		break;
	    }
	}
    }
    if(wholistens == -1)
	parentsig(SIGUSR1);
    return 0;
}

#ifndef MAP_ANON
#ifdef MAP_ANONYMOUS
#define MAP_ANON MAP_ANONYMOUS
#endif
#endif

static void *
sharemem(size_t len)
{
#ifdef MAP_ANON
    return mmap(0, len, PROT_READ|PROT_WRITE, MAP_ANON|MAP_SHARED, -1, 0);
#else
    int fd = open("/dev/zero", O_RDWR, 0);
    char *ptr = mmap(0, len, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
    close(fd);
    return ptr;
#endif
}

int
main(int argc, char *argv[])
{
    int i, ret;
    socklen_t salen;
    char *path, *pidfile;
#ifdef HAVE_UNIX_DOMAIN_SOCKETS
    struct sockaddr_un sa;
#else
    struct sockaddr_in sa;
#endif
    struct timeval tv;
    fd_set rdset;

    setproctitle_init(argc, argv);

    openlog("ovdb_server", L_OPENLOG_FLAGS | LOG_PID, LOG_INN_PROG);
    message_program_name = "ovdb_server";

    if(argc != 2 || strcmp(argv[1], SPACES))
        die("should be started by ovdb_init");
    message_handlers_warn(1, message_log_syslog_err);
    message_handlers_die(1, message_log_syslog_err);

    if (!innconf_read(NULL))
	exit(1);

    if(strcmp(innconf->ovmethod, "ovdb"))
        die("ovmethod not set to ovdb in inn.conf");

    read_ovdb_conf();

#ifdef HAVE_UNIX_DOMAIN_SOCKETS
    listensock = socket(AF_UNIX, SOCK_STREAM, 0);
#else
    listensock = socket(AF_INET, SOCK_STREAM, 0);
#endif
    if(listensock < 0)
        sysdie("cannot create socket");

    nonblocking(listensock, 1);

#ifdef HAVE_UNIX_DOMAIN_SOCKETS
    sa.sun_family = AF_UNIX;
    path = concatpath(innconf->pathrun, OVDB_SERVER_SOCKET);
    strlcpy(sa.sun_path, path, sizeof(sa.sun_path));
    unlink(sa.sun_path);
    free(path);
    ret = bind(listensock, (struct sockaddr *)&sa, sizeof sa);
#else
    sa.sin_family = AF_INET;
    sa.sin_port = htons(OVDB_SERVER_PORT);
    sa.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
    
    ret = bind(listensock, (struct sockaddr *)&sa, sizeof sa);

    if(ret != 0 && errno == EADDRNOTAVAIL) {
	sa.sin_family = AF_INET;
	sa.sin_port = htons(OVDB_SERVER_PORT);
	sa.sin_addr.s_addr = INADDR_ANY;
	ret = bind(listensock, (struct sockaddr *)&sa, sizeof sa);
    }
#endif

    if(ret != 0)
        sysdie("cannot bind socket");
    if(listen(listensock, MAXLISTEN) < 0)
        sysdie("cannot listen on socket");

    pidfile = concatpath(innconf->pathrun, OVDB_SERVER_PIDFILE);
    if(putpid(pidfile))
        exit(1);

    xsignal_norestart(SIGINT, sigfunc);
    xsignal_norestart(SIGTERM, sigfunc);
    xsignal_norestart(SIGHUP, sigfunc);

    xsignal_norestart(SIGUSR1, parentsig);
    xsignal_norestart(SIGCHLD, childsig);
    parent = getpid();

    children = sharemem(sizeof(struct child) * (ovdb_conf.numrsprocs+1));

    if(children == NULL)
        sysdie("cannot mmap shared memory");
    for(i = 0; i < ovdb_conf.numrsprocs+1; i++) {
	children[i].pid = -1;
	children[i].num = 0;
    }

    for(i = 0; i < ovdb_conf.numrsprocs; i++) {
	if((children[i].pid = serverproc(i)) == -1) {
	    for(i--; i >= 0; i--)
		kill(children[i].pid, SIGTERM);
	    exit(1);
	}
	children[i].started = time(NULL);
	sleep(1);
    }

    while(!signalled) {
	if(reap())
	    break;

	if(wholistens == -2) {
	    FD_ZERO(&rdset);
	    FD_SET(listensock, &rdset);
	    tv.tv_usec = 0;
	    tv.tv_sec = SELECT_TIMEOUT;
	    ret = select(listensock+1, &rdset, NULL, NULL, &tv);

	    if(ret == 1 && wholistens == -2) {
		salen = sizeof(sa);
		ret = accept(listensock, (struct sockaddr *)&sa, &salen);
		if(ret >= 0)
		   close(ret);
	    }
	} else {
	    pause();
	}
    }

    for(i = 0; i < ovdb_conf.numrsprocs; i++)
	if(children[i].pid != -1)
	    kill(children[i].pid, SIGTERM);

    while(wait(&ret) > 0)
	;

    unlink(pidfile);

    exit(0);
}


#endif /* USE_BERKELEY_DB */
