/* Services -- main source file.
 * Copyright (c) 1996-2009 Andrew Church <achurch@achurch.org>
 * Parts written by Andrew Kempe and others.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License, version 2, as
 * published by the Free Software Foundation.
 *
 * 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 (see the file COPYING); if not, write to the
 * Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

#include "services.h"
#include "modules.h"
#include "timeout.h"


/******** Global variables! ********/

/* Command-line options: (note that configuration variables are in init.c) */
const char *services_dir = SERVICES_DIR;/* -dir=dirname */
int   debug        = 0;			/* -debug */
int   readonly     = 0;			/* -readonly */
int   nofork       = 0;			/* -nofork */
int   noexpire     = 0;			/* -noexpire */
int   noakill      = 0;			/* -noakill */
int   forceload    = 0;			/* -forceload */


/* Set to 1 while we are linked to the network */
int linked = 0;

/* Set to 1 if we are to quit */
int quitting = 0;

/* Set to 1 if we are to quit after saving databases, or 2 to restart */
int delayed_quit = 0;

/* Contains a message as to why services is terminating */
char quitmsg[BUFSIZE] = "";

/* Input buffer - global, so we can dump it if something goes wrong */
char inbuf[BUFSIZE];

/* Socket for talking to server */
Socket *servsock = NULL;

/* Should we update the databases now? */
int save_data = 0;

/* At what time were we started? */
time_t start_time;

/* Were we unable to open the log? (and the error that occurred) */
int openlog_failed, openlog_errno;

/* Module callbacks (global so init.c can set them): */
int cb_connect       = -1;
int cb_save_data     = -1;
int cb_save_complete = -1;


/*************************************************************************/
/*************************************************************************/

/* Callbacks for uplink IRC server socket. */

/*************************************************************************/

/* Actions to perform when connection to server completes. */

void connect_callback(Socket *s, void *param_unused)
{
#ifdef CLEAN_COMPILE
    param_unused = param_unused;
#endif
    sock_set_blocking(s, 1);
    sock_setcb(s, SCB_READLINE, readfirstline_callback);
    send_server();
}

/*************************************************************************/

/* Actions to perform when connection to server is broken. */

void disconnect_callback(Socket *s, void *param)
{
    /* We are no longer linked */
    linked = 0;

    if (param == DISCONN_REMOTE || param == DISCONN_CONNFAIL) {
	int errno_save = errno;
	const char *msg = (param==DISCONN_REMOTE ? "Read error from server"
			   : "Connection to server failed");
	snprintf(quitmsg, sizeof(quitmsg),
		 "%s: %s", msg, strerror(errno_save));
	if (param == DISCONN_REMOTE) {
	    /* If we were already connected, make sure any changed data is
	     * updated before we terminate. */
	    delayed_quit = 1;
	    save_data = 1;
	} else {
	    /* The connection was never made in the first place, so we
	     * discard any changes (such as expirations) made on the
	     * assumption that either a configuration problem or other
	     * external problem exists.  Such changes will be saved the
	     * next time Services successfully connects to a server. */
	    quitting = 1;
	}
    }
    sock_setcb(s, SCB_READLINE, NULL);
}

/*************************************************************************/

/* Actions to perform when first line is read from socket. */

void readfirstline_callback(Socket *s, void *param_unused)
{
#ifdef CLEAN_COMPILE
    param_unused = param_unused;
#endif
    sock_setcb(s, SCB_READLINE, readline_callback);

    if (!sgets2(inbuf, sizeof(inbuf), s)) {
	/* This shouldn't happen, but just in case... */
	disconn(s);
	fatal("Unable to read greeting from server socket");
    }
    if (strnicmp(inbuf, "ERROR", 5) == 0) {
	/* Close server socket first to stop wallops, since the other
	 * server doesn't want to listen to us anyway */
	disconn(s);
	fatal("Remote server returned: %s", inbuf);
    }

    /* We're now linked to the network */
    linked = 1;

    /* Announce a logfile error if there was one */
    if (openlog_failed) {
	wallops(NULL, "Warning: couldn't open logfile: %s",
		strerror(openlog_errno));
	openlog_failed = 0;
    }

    /* Bring in our pseudo-clients */
    introduce_user(NULL);

    /* Let modules do their startup stuff */
    call_callback(NULL, cb_connect);

    /* Process the line we read in above */
    process();
}

/*************************************************************************/

/* Actions to perform when subsequent lines are read from socket. */

void readline_callback(Socket *s, void *param_unused)
{
#ifdef CLEAN_COMPILE
    param_unused = param_unused;
#endif
    if (sgets2(inbuf, sizeof(inbuf), s))
	process();
}

/*************************************************************************/
/*************************************************************************/

/* Subroutine to save databases. */

static void do_save_data(void)
{
    if (!lock_data()) {
	if (errno == EEXIST) {
	    log("warning: databases are locked, not updating");
	    wallops(NULL,
		    "\2Warning:\2 Databases are locked, and cannot be updated."
		    "  Remove the `%s%s%s' file to allow database updates.",
		    *LockFilename=='/' ? "" : services_dir,
		    *LockFilename=='/' ? "" : "/", LockFilename);
	} else {
	    log_perror("warning: unable to lock databases, not updating");
	    wallops(NULL, "\2Warning:\2 Unable to lock databases; databases"
			  " will not be updated.");
	}
	call_callback_1(NULL, cb_save_complete, 0);
    } else {
	if (debug)
	    log("debug: Saving databases");
	call_callback(NULL, cb_save_data);
	if (!unlock_data()) {
	    log_perror("warning: unable to unlock databases");
	    wallops(NULL,
		    "\2Warning:\2 Unable to unlock databases; future database"
		    " updates may fail until the `%s%s%s' file is removed.",
		    *LockFilename=='/' ? "" : services_dir,
		    *LockFilename=='/' ? "" : "/", LockFilename);
	}
	call_callback_1(NULL, cb_save_complete, 1);
    }
}

/*************************************************************************/

/* Main routine.  (What does it look like? :-) ) */

int main(int ac, char **av, char **envp)
{
    volatile time_t last_update; /* When did we last update the databases? */
    volatile uint32 last_check;  /* When did we last check timeouts? */


    /*** Initialization stuff. ***/

    if (init(ac, av) < 0) {
	fprintf(stderr, "Initialization failed, exiting.\n");
	return 1;
    }


    /* Set up timers. */
    last_send   = time(NULL);
    last_update = time(NULL);
    last_check  = time(NULL);

    /* The signal handler routine will drop back here with quitting != 0
     * if it gets called. */
    DO_SIGSETJMP();


    /*** Main loop. ***/

    while (!quitting) {
	time_t now = time(NULL);
	int32 now_msec = time_msec();

	if (debug >= 2)
	    log("debug: Top of main loop");

	if (!readonly && (save_data || now-last_update >= UpdateTimeout)) {
	    do_save_data();
	    save_data = 0;
	    last_update = now;
	}
	if (delayed_quit)
	    break;

	if (sock_isconn(servsock)) {
	    if (PingFrequency && now - last_send >= PingFrequency)
		send_cmd(NULL, "PING :%s", ServerName);
	}

	if (now_msec - last_check >= TimeoutCheck) {
	    check_timeouts();
	    last_check = now_msec;
	}

	check_sockets();

	if (!MergeChannelModes)
	    set_cmode(NULL, NULL);  /* flush out any mode changes made */
    }


    /*** Cleanup stuff. ***/

    /* Check for restart instead of exit */
    if (delayed_quit == 2) {
	if (!*quitmsg)
	    strscpy(quitmsg, "Restarting", sizeof(quitmsg));
	cleanup();
	execve(SERVICES_BIN, av, envp);
	if (!readonly) {
	    int errno_save = errno;
	    open_log();
	    errno = errno_save;
	    log_perror("Restart failed");
	    close_log();
	}
	return 1;
    }

    /* Disconnect and exit */
    cleanup();
    return 0;
}

/*************************************************************************/
