/* Initalization and related routines.
 *
 * IRC Services is copyright (c) 1996-2009 Andrew Church.
 *     E-mail: <achurch@achurch.org>
 * Parts written by Andrew Kempe and others.
 * This program is free but copyrighted software; see the file COPYING for
 * details.
 */

#include "services.h"
#include "conffile.h"
#include "messages.h"
#include "modules.h"
#include "language.h"
#include "version.h"

#if HAVE_SETGRENT
# include <grp.h>
#endif
#if HAVE_UMASK
# include <sys/stat.h>	/* for umask() on some systems */
#endif
#if HAVE_GETSETRLIMIT
# include <sys/resource.h>
#endif

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

/* Callbacks used in this file: */
static int cb_introduce_user = -1;
static int cb_cmdline        = -1;

/*************************************************************************/
/************************* Configuration options *************************/
/*************************************************************************/

/* Configurable variables: */

char **LoadModules;
int    LoadModules_count;

char * RemoteServer;
int32  RemotePort;
char * RemotePassword;
char * LocalHost;
int32  LocalPort;

char * ServerName;
char * ServerDesc;
char * ServiceUser;
char * ServiceHost;

char * LogFilename;
char * PIDFilename;
char * MOTDFilename;
char * LockFilename;

int16  DefTimeZone;

int    NoBouncyModes;
int    NoSplitRecovery;
int    StrictPasswords;
int32  BadPassLimit;
time_t BadPassTimeout;
int32  BadPassWarning;
time_t UpdateTimeout;
time_t WarningTimeout;
int32  ReadTimeout;
int32  TimeoutCheck;
time_t PingFrequency;
int32  MergeChannelModes;
int32  TotalNetBufferSize;
int32  NetBufferSize;
int32  NetBufferLimitInactive;
int32  NetBufferLimitIgnore;

char * GuestNickPrefix;
int    LogMaxUsers;
int    EnableGetpass;
int    WallGetpass;
int    WallSetpass;

/* Routines to handle special directives: */
static int do_DefTimeZone(const char *filename, int linenum, char *param);
static int do_LoadModule(const char *filename, int linenum, char *param);
static int do_RunGroup(const char *filename, int linenum, char *param);
static int do_ServiceUser(const char *filename, int linenum, char *param);
static int do_Umask(const char *filename, int linenum, char *param);

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

/* List of directives for ircservices.conf (main configuration file): */

static ConfigDirective main_directives[] = {
    { "BadPassLimit",     { { CD_POSINT, 0, &BadPassLimit } } },
    { "BadPassTimeout",   { { CD_TIME, 0, &BadPassTimeout } } },
    { "BadPassWarning",   { { CD_POSINT, 0, &BadPassWarning } } },
    { "DefTimeZone",      { { CD_FUNC, 0, do_DefTimeZone } } },
    { "EnableGetpass",    { { CD_SET, 0, &EnableGetpass } } },
    { "ExpireTimeout",    { { CD_DEPRECATED, 0, NULL } } },
    { "GuestNickPrefix",  { { CD_STRING, CF_DIRREQ, &GuestNickPrefix } } },
    { "LoadModule",       { { CD_FUNC, 0, do_LoadModule } } },
    { "LocalAddress",     { { CD_STRING, 0, &LocalHost },
                            { CD_PORT, CF_OPTIONAL, &LocalPort } } },
    { "LockFilename",     { { CD_STRING, CF_DIRREQ, &LockFilename } } },
    { "LogFilename",      { { CD_STRING, CF_DIRREQ, &LogFilename } } },
    { "LogMaxUsers",      { { CD_SET, 0, &LogMaxUsers } } },
    { "MergeChannelModes",{ { CD_TIMEMSEC, 0, &MergeChannelModes } } },
    { "MOTDFilename",     { { CD_STRING, CF_DIRREQ, &MOTDFilename } } },
    { "NetBufferLimit",   { { CD_POSINT, 0, &NetBufferLimitInactive },
                            { CD_POSINT, CF_OPTIONAL, &NetBufferLimitIgnore}}},
    { "NetBufferSize",    { { CD_POSINT, 0, &TotalNetBufferSize },
                            { CD_POSINT, CF_OPTIONAL, &NetBufferSize } } },
    { "NoBouncyModes",    { { CD_SET, 0, &NoBouncyModes } } },
    { "NoSplitRecovery",  { { CD_SET, 0, &NoSplitRecovery } } },
    { "PIDFilename",      { { CD_STRING, 0, &PIDFilename } } },
    { "PingFrequency",    { { CD_TIME, 0, &PingFrequency } } },
    { "ReadTimeout",      { { CD_TIMEMSEC, 0, &ReadTimeout } } },
    { "RemoteServer",     { { CD_STRING, CF_DIRREQ, &RemoteServer },
                            { CD_PORT, 0, &RemotePort },
                            { CD_STRING, 0, &RemotePassword } } },
    { "RunGroup",         { { CD_FUNC, 0, do_RunGroup } } },
    { "ServerDesc",       { { CD_STRING, CF_DIRREQ, &ServerDesc } } },
    { "ServerName",       { { CD_STRING, CF_DIRREQ, &ServerName } } },
    { "ServiceUser",      { { CD_FUNC, CF_DIRREQ, do_ServiceUser } } },
    { "StrictPasswords",  { { CD_SET, 0, &StrictPasswords } } },
    { "TimeoutCheck",     { { CD_TIMEMSEC, CF_DIRREQ, &TimeoutCheck } } },
    { "Umask",            { { CD_FUNC,  0, do_Umask } } },
    { "UpdateTimeout",    { { CD_TIME, CF_DIRREQ, &UpdateTimeout } } },
    { "WallGetpass",      { { CD_SET, 0, &WallGetpass } } },
    { "WallSetpass",      { { CD_SET, 0, &WallSetpass } } },
    { "WarningTimeout",   { { CD_TIME, CF_DIRREQ, &WarningTimeout } } },
    { NULL }
};

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

/* read_config():  Read the main configuration file.  If an error occurs
 *                 while reading the file or a required directive is not
 *                 found, print and log an appropriate error message and
 *                 return 0; otherwise, return 1.
 */

static int read_config(void)
{
    int retval = 1;

    if (!configure(NULL, main_directives, CONFIGURE_READ | CONFIGURE_SET))
	return 0;

    if (TotalNetBufferSize) {
	if (TotalNetBufferSize < NET_MIN_BUFSIZE*2) {
	    config_error(IRCSERVICES_CONF, 0,
			 "Buffer size limit for NetBufferSize must be at"
			 " least %d", NET_MIN_BUFSIZE*2);
	    retval = 0;
	} else {
	    /* Make sure it's a multiple of NET_MIN_BUFSIZE */
	    TotalNetBufferSize /= NET_MIN_BUFSIZE;
	    TotalNetBufferSize *= NET_MIN_BUFSIZE;
	    if (NetBufferSize) {
		if (NetBufferSize < NET_MIN_BUFSIZE*2) {
		    config_error(IRCSERVICES_CONF, 0,
				 "Per-connection buffer size limit for"
				 " NetBufferSize must be at least %d",
				 NET_MIN_BUFSIZE*2);
		    retval = 0;
		} else if (NetBufferSize > TotalNetBufferSize) {
		    config_error(IRCSERVICES_CONF, 0,
				 "Per-connection buffer size limit for"
				 " NetBufferSize must be no more than total"
				 " limit");
		    retval = 0;
		} else {
		    NetBufferSize /= NET_MIN_BUFSIZE;
		    NetBufferSize *= NET_MIN_BUFSIZE;
		}
	    }
	}
    } /* if (TotalNetBufferSize) */

    if (NetBufferLimitInactive) {
	if (!TotalNetBufferSize) {
	    NetBufferLimitInactive = NetBufferLimitIgnore = 0;
	} else {
	    if (NetBufferLimitInactive > 99 || NetBufferLimitIgnore > 99) {
		config_error(IRCSERVICES_CONF, 0,
			     "Thresholds for NetBufferLimit must be between"
			     " 1 and 99 inclusive");
		retval = 0;
	    }
	    if (NetBufferLimitIgnore
	     && NetBufferLimitIgnore < NetBufferLimitInactive
	    ) {
		config_error(IRCSERVICES_CONF, 0,
			     "Ignore threshold for NetBufferLimit must be"
			     " greater than or equal to inactive threshold");
		retval = 0;
	    }
	}
    }

    return retval;
}

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

/* Configuration directive callback functions: */

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

#define TZ_MAXLEN	16

static int do_DefTimeZone(const char *filename, int linenum, char *param)
{
    static const char *origTZ = NULL;
    static char newTZ[TZ_MAXLEN+1];
    static char tzbuf[TZ_MAXLEN+4];  /* for setting TZ */

    if (!filename) {
	switch (linenum) {
	  case CDFUNC_INIT:
	    /* Prepare for reading config file */
	    *newTZ = 0;
	    if (!origTZ) {
		/* Obtain current value of TZ environment variable (once
		 * only, at start of program), and truncate to TZ_MAXLEN */
		origTZ = getenv("TZ");
		if (!origTZ)
		    origTZ = "";
		if (strlen(origTZ) > TZ_MAXLEN) {
		    static char new_origTZ[TZ_MAXLEN+1];
		    memcpy(new_origTZ, origTZ, TZ_MAXLEN);
		    new_origTZ[TZ_MAXLEN] = 0;
		    origTZ = new_origTZ;
		}
	    }
	    break;
	  case CDFUNC_SET:
	    /* Copy data to config variables */
	    if (!origTZ) {
		log("BUG: origTZ not set in do_DefTimeZone/CDFUNC_SET");
		break;
	    }
	    snprintf(tzbuf, sizeof(tzbuf), "TZ=%s", *newTZ ? newTZ : origTZ);
	    if (putenv(tzbuf) < 0) {
		log("Warning: putenv(%s) failed, time zone may be incorrect",
		    tzbuf);
	    }
	    break;
	  case CDFUNC_DECONFIG:
	    /* Reset to initial values */
	    if (!origTZ) {
		log("BUG: origTZ not set in do_DefTimeZone/CDFUNC_DECONFIG");
		break;
	    }
	    snprintf(tzbuf, sizeof(tzbuf), "TZ=%s", origTZ);
	    if (putenv(tzbuf) < 0) {
		log("Warning: putenv(%s) failed, time zone may be incorrect",
		    tzbuf);
	    }
	    break;
	} /* switch (linenum) */
    } else {  /* filename != NULL, process parameter */
	if (strlen(param) > TZ_MAXLEN) {
	    config_error(filename, linenum, "DefTimeZone parameter must not"
			 " be longer than %d characters", TZ_MAXLEN);
	    return 0;
	} else {
	    strscpy(newTZ, param, sizeof(newTZ));
	}
    }
    return 1;
}

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

static int do_LoadModule(const char *filename, int linenum, char *param)
{
    static char **new_LoadModules = NULL;
    static int new_LoadModules_count = 0;
    int i;

    if (!filename) {
	switch (linenum) {
	  case CDFUNC_INIT:
	    /* Prepare for reading config file: clear out "new" array */
	    ARRAY_FOREACH (i, new_LoadModules)
		free(new_LoadModules[i]);
	    free(new_LoadModules);
	    new_LoadModules = NULL;
	    new_LoadModules_count = 0;
	    break;
	  case CDFUNC_SET:
	    /* Copy data to config variables */
	    ARRAY_FOREACH (i, LoadModules)
		free(LoadModules[i]);
	    free(LoadModules);
	    LoadModules = new_LoadModules;
	    LoadModules_count = new_LoadModules_count;
	    new_LoadModules = NULL;
	    new_LoadModules_count = 0;
	    break;
	  case CDFUNC_DECONFIG:
	    /* Clear out config variables */
	    ARRAY_FOREACH (i, LoadModules)
		free(LoadModules[i]);
	    free(LoadModules);
	    LoadModules = NULL;
	    LoadModules_count = 0;
	    break;
	}
	return 1;
    } /* if (!filename) */

    /* We can't use ARRAY_EXTEND because SIGUSR1 (for srealloc()) may not
     * be ready yet */
    if (new_LoadModules_count+1 < new_LoadModules_count) {
	config_error(filename, linenum, "LoadModule: too many modules!");
	return 0;
    }
    new_LoadModules = realloc(new_LoadModules,
			      sizeof(char *) * (new_LoadModules_count+1));
    param = strdup(param);
    if (!new_LoadModules || !param) {
	config_error(filename, linenum, "LoadModule: out of memory!");
	return 0;
    }
    new_LoadModules[new_LoadModules_count++] = param;
    return 1;
}

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

static int do_RunGroup(const char *filename, int linenum, char *param)
{
#ifndef SIZEOF_GID_T
    config_error(filename, linenum,
		 "RunGroup: groups not supported on this system");
    return 0;
#else
    static gid_t groupnum = -1;
    long tmp;

    if (filename) {

	if (*param == '=') {
	    if (!param[1]) {
		config_error(filename, linenum,
			     "RunGroup: group number required after `='");
		return 0;
	    }
	    errno = 0;
	    tmp = strtol(param+1, &param, 0);
# if SIZEOF_GID_T >= 4
	    if (errno == ERANGE)
# else
	    if (tmp < -32768 || tmp > 65535)
# endif
	    {
		config_error(filename, linenum,
			     "RunGroup: group number out of range (0..%ld)",
# if SIZEOF_GID_T >= 4
			     0x7FFFFFFFL
# else
			     65535L
# endif
		);
		return 0;
	    }
	    groupnum = tmp;
	} else { /* *param != '=' */
# if !HAVE_SETGRENT
	    config_error(filename, linenum,
			 "RunGroup: group names not supported on this system");
	    return 0;
# else
	    struct group *gr;
	    setgrent();
	    while ((gr = getgrent()) != NULL) {
		if (strcmp(gr->gr_name, param) == 0)
		    break;
	    }
	    endgrent();
	    if (!gr) {
		config_error(filename, linenum,
			     "RunGroup: unknown group `%s'", param);
		return 0;
	    }
	    groupnum = gr->gr_gid;
# endif
	}

    } else { /* !filename */

# if HAVE_SETREGID
	if (setregid(groupnum, groupnum) < 0) {
# else
	if (setegid(groupnum) < 0 || setgid(groupnum) < 0) {
# endif
	    config_error(filename, linenum,
			 "RunGroup: unable to set group: %s", strerror(errno));
	    return 0;
	}

    } /* if (filename) */

    return 1;

#endif  /* have gid_t? */
}

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

static int do_ServiceUser(const char *filename, int linenum, char *param)
{
    char *s;
    static char *new_ServiceUser = NULL, *new_ServiceHost = NULL;

    if (filename) {
	/* Make sure the string has an @ and there's at least one character
	 * on each side */
	s = strchr(param, '@');
	if (!s || s == param || s[1] == 0) {
	    config_error(filename, linenum,
			 "`user@host' string required for ServiceUser");
	    return 0;
	}
	/* Store user and hostname parts (after freeing any old ones) */
	*s++ = 0;
	free(new_ServiceUser);
	free(new_ServiceHost);
	new_ServiceUser = strdup(param);
	new_ServiceHost = strdup(s);
	if (!new_ServiceUser || !new_ServiceHost) {
	    free(new_ServiceUser);  /* still alloc'ed if ServiceHost failed */
	    new_ServiceUser = NULL;
	    config_error(filename, linenum, "Out of memory");
	    return 0;
	}
    } else if (linenum == CDFUNC_SET) {
	/* Copy new values to config variables and clear */
	if (new_ServiceUser && new_ServiceHost) {  /* paranoia */
	    free(ServiceUser);
	    free(ServiceHost);
	    ServiceUser = new_ServiceUser;
	    ServiceHost = new_ServiceHost;
	} else {
	    free(new_ServiceUser);
	    free(new_ServiceHost);
	}
	new_ServiceUser = new_ServiceHost = NULL;
    } else if (linenum == CDFUNC_DECONFIG) {
	/* Reset to defaults */
	free(ServiceUser);
	free(ServiceHost);
	ServiceUser = NULL;
	ServiceHost = NULL;
    }
    return 1;
}

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

static int do_Umask(const char *filename, int linenum, char *param)
{
#if !HAVE_UMASK
    config_error(filename, linenum, "Umask is not supported on this system");
    return 0;
#else
    char *s;
    static int umask_val = -1;

    if (filename) {
	umask_val = strtol(param, &s, 8);
	if (*s || umask_val < 0 || umask_val > 0777) {
	    config_error(filename, linenum,
			 "Expected an octal value between 000 and 777");
	    return 0;
	}
    } else {
	if (umask_val >= 0)
	    umask(umask_val);
	umask_val = -1;
    }
    return 1;
#endif  /* HAVE_UMASK */
}

/*************************************************************************/
/*********************** Introducing pseudoclients ***********************/
/*************************************************************************/

/* If `user' is the name of a Services pseudo-client, send a NICK command
 * for that given pseudo-client.  If `user' is NULL, send NICK commands for
 * all the pseudo-clients.  Return 1 if we sent a NICK command, else 0.
 */

#define NICK(nick,name,modes) do { \
    snprintf(modebuf, sizeof(modebuf), "%s%s", (modes), pseudoclient_modes); \
    send_nick((nick), ServiceUser, ServiceHost, ServerName, (name), modebuf); \
    retval = 1; \
} while (0)

int introduce_user(const char *user)
{
    int retval;

    retval = call_callback_1(NULL, cb_introduce_user, user);
    if (user == NULL) {
	if (retval > 0)
	    log("introduce_user(): callback returned nonzero for user==NULL");
	retval = 1;
    }

    /* Watch out for infinite loops... */
    if (retval) {
#define LTSIZE 20
	static int lasttimes[LTSIZE];
	if (lasttimes[0] >= time(NULL)-3)
	    fatal("introduce_user() loop detected");
	memmove(lasttimes, lasttimes+1, sizeof(lasttimes)-sizeof(*lasttimes));
	lasttimes[LTSIZE-1] = time(NULL);
#undef LTSIZE
    }

    return retval;
}

#undef NICK

/*************************************************************************/
/************************* Command-line parsing **************************/
/*************************************************************************/

/* Parse command-line options.  If `call_modules' is zero, parse main
 * options and ignore unrecognized ones; if nonzero, ignore main options,
 * call "command line" callback to check module options, and fail on
 * unrecognized ones.  Return 0 if all went well, -1 for an error with an
 * option, or 1 if Services should exit successfully.  Calls exit(0) if
 * "-help", "--help", or "-h" is present.
 */

static int parse_options(int ac, char **av, int call_modules)
{
    int i;
    char *s, *t;

    if (!call_modules)  /* only initialize it once */
	debug = 0;

    for (i = 1; i < ac; i++) {
	s = av[i];
	if (*s == '-') {
	    s++;
	    if (strncmp(s, "dir", 3) == 0 && (!s[3] || s[3] == '=')) {
		if (!call_modules) {
		    if (!s[3]) {
			fprintf(stderr, "-dir requires a parameter\n");
			return -1;
		    }
		    services_dir = s+4;
		}
	    } else if (strncmp(s, "remote", 6) == 0 && (!s[6] || s[6]=='=')) {
		if (!call_modules) {
		    if (!s[6]) {
			fprintf(stderr, "-remote requires hostname[:port]\n");
			return -1;
		    }
		    s += 7;
		    t = strchr(s, ':');
		    if (t) {
			*t = 0;
			if (atoi(t+1) > 0)
			    RemotePort = atoi(t+1);
			else {
			    fprintf(stderr, "-remote: port number must be a positive integer.  Using default.");
			    return -1;
			}
		    }
		    free(RemoteServer);
		    RemoteServer = strdup(s);
		    if (!RemoteServer) {
			fprintf(stderr, "Out of memory\n");
			exit(-1);
		    }
		    if (t)  /* put the colon back for next time around */
			*t = ':';
		}
	    } else if (strncmp(s, "log", 3) == 0 && (!s[3] || s[3] == '=')) {
		if (!call_modules) {
		    if (!s[3]) {
			fprintf(stderr, "-log requires a parameter\n");
			return -1;
		    }
		    free(LogFilename);
		    LogFilename = s+4;
		}
	    } else if (strcmp(s, "debug") == 0) {
		if (!call_modules)
		    debug++;
	    } else if (strcmp(s, "readonly") == 0) {
		if (!call_modules)
		    readonly = 1;
	    } else if (strcmp(s, "nofork") == 0) {
		if (!call_modules)
		    nofork = 1;
	    } else if (strcmp(s, "noexpire") == 0) {
		if (!call_modules)
		    noexpire = 1;
	    } else if (strcmp(s, "noakill") == 0) {
		if (!call_modules)
		    noakill = 1;
	    } else if (strcmp(s, "forceload") == 0) {
		if (!call_modules)
		    forceload = 1;
	    } else if (strcmp(s, "h") == 0 || strcmp(s, "help") == 0
		       || strcmp(s, "-help") == 0) {
		fputs(
"The following options are recognized:\n"
"	-dir=directory		Directory containing Services' data files\n"
"				    (e.g. /usr/local/lib/ircservices)\n"
"	-remote=server[:port]	Remote server to connect to\n"
"	-log=filename		Services log filename (e.g. services.log)\n"
"	-debug			Enable debugging mode--more info sent to log\n"
"				    (give option more times for more info)\n"
"	-readonly		Enable read-only mode--no changes to\n"
"				    databases allowed, .db files and log\n"
"				    not written\n"
"	-nofork			Do not fork after startup; log messages will\n"
"				    be written to terminal (as well as to\n"
"				    the log file if not in read-only mode)\n"
"	-noexpire		Prevents all expirations (nicknames, channels,\n"
"				    akills, session limit exceptions, etc.)\n"
"	-noakill		Disables autokill checking\n"
"	-forceload		Try to load as much of the databases as\n"
"				    possible, even if errors are encountered\n"
"Other options may be available depending on loaded modules; see the manual\n"
"for details.\n"
, stdout);
		exit(0);
	    } else if (call_modules) {
		int res;
		t = strchr(s, '=');
		if (t)
		    *t++ = 0;
		res = call_callback_2(NULL, cb_cmdline, s, t);
		switch (res) {
		  case 0:
		    fprintf(stderr, "Unknown option -%s.  Use \"-help\" for"
			    " help.\n", s);
		    return -1;
		  case 1:
		    break;
		  case 2:
		    return -1;
		  case 3:
		    return 1;
		  default:
		    log("init: bad return value (%d) from command line"
			" callback for `-%s%s%s'", res, s, t ? "=" : "", t);
		    return -1;
		}
	    }
	} else {
	    fprintf(stderr, "Non-option arguments not allowed\n");
	    return -1;
	}
    }
    return 0;
}

/*************************************************************************/
/*************************** PID file handling ***************************/
/*************************************************************************/

/* Remove our PID file.  Done at exit. */

static void remove_pidfile(void)
{
    remove(PIDFilename);
}

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

/* Create our PID file and write the PID to it.  Returns nonzero on
 * success, zero on failure.
 */

static int write_pidfile(void)
{
    FILE *pidfile;

    pidfile = fopen(PIDFilename, "w");
    if (!pidfile)
	return 0;
    fprintf(pidfile, "%d\n", (int)getpid());
    fclose(pidfile);
    atexit(remove_pidfile);
    return 1;
}

/*************************************************************************/
/********************** Main initialization routine **********************/
/*************************************************************************/

/* Overall initialization routine.  Returns 0 on success, -1 on failure.
 * Never returns failure after forking / closing standard file descriptors.
 */

int init(int ac, char **av)
{
    int i;
    int openlog_failed = 0, openlog_errno = 0;
    int started_from_term = isatty(0) && isatty(1) && isatty(2);


#ifdef MEMCHECKS
    /* Initialize memory log, to keep track of allocations. */
    open_memory_log();
    /* Account for runtime memory. */
    init_memory();
#endif

    /* Parse command-line options; exit if an error occurs. */
    if (parse_options(ac, av, 0) < 0)
	return -1;

    /* Chdir to Services data directory. */
    if (chdir(services_dir) < 0) {
	fprintf(stderr, "chdir(%s): %s\n", services_dir, strerror(errno));
	return -1;
    }

    /* Read configuration file; exit if there are problems. */
    if (!read_config())
	return -1;

    /* Re-parse command-line options (to override configuration file). */
    parse_options(ac, av, 0);

    /* Open logfile, and complain if we couldn't. */
    if (!open_log()) {
	openlog_errno = errno;
	if (started_from_term) {
	    fprintf(stderr, "Warning: unable to open log file %s: %s\n",
		    LogFilename, strerror(errno));
	} else {
	    openlog_failed = 1;
	}
    }

    /* Announce ourselves to the logfile. */
    if (debug || readonly || noexpire) {
	log("IRC Services %s starting up (options:%s%s%s)",
	    version_number,
	    debug ? " debug" : "",
	    readonly ? " readonly" : "",
	    noexpire ? " noexpire" : "");
    } else {
	log("IRC Services %s starting up", version_number);
    }
#ifdef FORCED_GCC_2_96
    log("Warning: This program was compiled with GCC 2.96, which is"
	" unsupported.  Please read the documentation before reporting any"
	" problems."); 
#endif
    start_time = time(NULL);

    /* If in read-only mode, close the logfile again. */
    if (readonly)
	close_log();


    /* If DUMPCORE is defined and the OS supports get/setrlimit(), then
     * attempt to remove, or at least raise to maximum, the core file size
     * limit. */
#if defined(DUMPCORE) && HAVE_GETSETRLIMIT
    {
	struct rlimit rl = {RLIM_INFINITY,RLIM_INFINITY};
	if (setrlimit(RLIMIT_CORE, &rl) < 0) {
	    log_perror("setrlimit(RLIMIT_CORE, RLIM_INFINITY)");
	    if ((i = getrlimit(RLIMIT_CORE, &rl)) < 0
	     || rl.rlim_cur >= rl.rlim_max
	    ) {
		if (i < 0)
		    log_perror("getrlimit(RLIMIT_CORE)");
		log("Unable to set core file size limit; core files %s.",
		    i < 0 || rl.rlim_cur == 0
			? "will not be generated" : "may be truncated");
	    } else {
		rl.rlim_cur = rl.rlim_max;
		if (setrlimit(RLIMIT_CORE, &rl) < 0) {
		    log_perror("setrlimit(RLIMIT_CORE, %ld)",
			       (long)rl.rlim_cur);
		    log("Unable to set core file size limit; core files may"
			" be truncated.");
		} else {
		    log("Core file size limited to %ldkB; core files may be"
			" truncated.",
			(long)(rl.rlim_cur<1024 ? 1 : rl.rlim_cur/1024));
		}
	    }
	} /* if (setrlimit(...) < 0) */
    } /* setrlimit() block */
#endif


    /* Initialize pseudo-random number generator. */
    srand(time(NULL) ^ getppid() ^ getpid()<<16);

    /* Initialize module system.  This should be called before any
     * callbacks are registered. */
    if (!modules_init(ac,av))
	return -1;

    /* Register our (and main.c's) callbacks. */
    cb_cmdline        = register_callback(NULL, "command line");
    cb_introduce_user = register_callback(NULL, "introduce_user");
    cb_connect        = register_callback(NULL, "connect");
    cb_save_data      = register_callback(NULL, "save data");
    cb_save_complete  = register_callback(NULL, "save data complete");
    if (cb_cmdline < 0 || cb_introduce_user < 0 || cb_connect < 0
     || cb_save_data < 0 || cb_save_complete < 0
    ) {
	log("init(): Unable to register callbacks");
	return -1;
    }

    /* Call other initialization routines.  These are mainly (right now
     * only) for adding callbacks. */
    if (!user_init(ac,av) || !channel_init(ac,av) || !server_init(ac,av)
     || !process_init(ac,av) || !messages_init(ac,av)
     || !actions_init(ac,av) || !send_init(ac,av)
    ) {
	return -1;
    }

    /* Initialize multi-language support. */
    if (!lang_init())
	return -1;
    if (debug)
	log("debug: Loaded languages");

    /* Load modules. */
    ARRAY_FOREACH (i, LoadModules) {
	if (!load_module(LoadModules[i])) {
	    log("Error loading modules, aborting");
	    return -1;
	}
    }
    if (debug)
	log("debug: Loaded modules");

    /* Check command-line arguments in modules, and exit if a module directs
     * us to. */
    i = parse_options(ac, av, 1);
    if (i != 0) {
	if (i < 0) {
	    cleanup();
	    return -1;
	} else {
	    call_callback(NULL, cb_save_data);
	    cleanup();
	    exit(0);
	}
    }

    /* Make sure a protocol module was loaded. */
    if (protocol_features & PF_UNSET) {
	fprintf(stderr,
		"No protocol module has been loaded!  Make sure to include a LoadModule\n"
		"directive for the appropriate module in the `%s' file.\n",
		IRCSERVICES_CONF);
	cleanup();
	return -1;
    }


    /* So far so good; let the user know everything is okay. */
    if (!nofork)
	fprintf(stderr, "Initialization successful, starting IRC Services.\n");

    /* Detach ourselves if requested. */
    if (!nofork) {
	if ((i = fork()) < 0) {
	    perror("fork()");
	    return -1;
	} else if (i != 0) {
#ifdef MEMCHECKS
	    /* Avoid a bogus "XXX bytes leaked on exit" message for the
	     * parent. */
	    uninit_memory();
#endif
	    exit(0);
	}
	if (started_from_term) {
	    close(0);
	    close(1);
	    close(2);
	}
	if (setpgid(0, 0) < 0) {
	    perror("setpgid()");
	    return -1;
	}
    }

    /*
     * Everything from here down needs to be done in the child process
     * (when forking).
     */

    /* Write our PID to the PID file. */
    if (!write_pidfile())
	log_perror("Warning: cannot write to PID file %s", PIDFilename);

    /* Set up signal handlers. */
    init_signals();

    /* Connect to the remote server. */
    servsock = sock_new();
    if (!servsock)
	fatal_perror("Can't create server socket");
    sock_setcb(servsock, SCB_CONNECT, connect_callback);
    sock_setcb(servsock, SCB_DISCONNECT, disconnect_callback);
    if (conn(servsock, RemoteServer, RemotePort, LocalHost, LocalPort) < 0)
	fatal_perror("Can't connect to server (%s:%d)",
		     RemoteServer, RemotePort);
    if (debug)
	log("Initiated connection to %s:%d", RemoteServer, RemotePort);

    /* Return success (connect_callback() will handle the rest). */
    return 0;
}

/*************************************************************************/
/**************************** Reconfiguration ****************************/
/*************************************************************************/

int reconfigure(void)
{
    char *old_RemoteServer, *old_RemotePassword, *old_LocalHost;
    int old_RemotePort, old_LocalPort;
    char *old_ServerName, *old_ServerDesc, *old_ServiceUser, *old_ServiceHost;
    char *old_LogFilename, *old_PIDFilename;
    char **old_LoadModules;
    int old_LoadModules_count;
    int LoadModules_insert;  /* where to insert unloadable modules */
    int i, j;
    int retval = 1;

    /* First save any data that we need after re-reading the conf file */
    old_RemoteServer = sstrdup(RemoteServer);
    old_RemotePort = RemotePort;
    old_RemotePassword = sstrdup(RemotePassword);
    old_LocalHost = LocalHost ? sstrdup(LocalHost) : NULL;
    old_LocalPort = LocalPort;
    old_ServerName = sstrdup(ServerName);
    old_ServerDesc = sstrdup(ServerDesc);
    old_ServiceUser = sstrdup(ServiceUser);
    old_ServiceHost = sstrdup(ServiceHost);
    old_LogFilename = sstrdup(LogFilename);
    old_PIDFilename = sstrdup(PIDFilename);
    old_LoadModules = LoadModules;
    old_LoadModules_count = LoadModules_count;

    /* Re-read the configuration */
    if (!configure(NULL, main_directives, CONFIGURE_READ))
	return 0;
    /* Prevent current LoadModules (now old_LoadModules) from being freed */
    LoadModules = NULL;
    LoadModules_count = 0;
    /* Copy new values to configuration variables */
    configure(NULL, main_directives, CONFIGURE_SET);

    /* Deal with configuration changes */
    if (stricmp(RemoteServer, old_RemoteServer) != 0
     || RemotePort != old_RemotePort
     || strcmp(RemotePassword, old_RemotePassword) != 0)
	log("warning: reconfigure: new RemoteServer value will not take"
	    " effect until restart");
    if ((!old_LocalHost && LocalHost) || (old_LocalHost && !LocalHost)
     || (LocalHost && stricmp(LocalHost, old_LocalHost) != 0)
     || LocalPort != old_LocalPort)
	log("warning: reconfigure: new LocalHost value will not take"
	    " effect until restart");
    if (strcmp(ServerName, old_ServerName) != 0)
	log("warning: reconfigure: new ServerName value will not take"
	    " effect until restart");
    if (strcmp(ServerDesc, old_ServerDesc) != 0)
	log("warning: reconfigure: new ServerDesc value will not take"
	    " effect until restart");
    if ((!old_ServiceUser && ServiceUser) || (!old_ServiceHost && ServiceHost)
     || (ServiceUser && strcmp(ServiceUser, old_ServiceUser) != 0)
     || (ServiceHost && strcmp(ServiceHost, old_ServiceHost) != 0))
	log("warning: reconfigure: new ServiceUser value will not take"
	    " effect until restart");
    if (strcmp(LogFilename, old_LogFilename) != 0) {
	log("reconfigure: LogFilename changed, closing log file");
	if (reopen_log()) {
	    log("reconfigure: LogFilename changed, writing to new log file");
	} else {
	    log("warning: reconfigure: unable to open new log file `%s',"
		" reverting to old file `%s'", LogFilename, old_LogFilename);
	    free(LogFilename);
	    LogFilename = old_LogFilename;
	    old_LogFilename = NULL;  /* don't free it below */
	}
    }
    if (strcmp(PIDFilename, old_PIDFilename) != 0) {
	if (write_pidfile()) {
	    /* Successfully wrote the new PID file, so delete the old one */
	    remove(old_PIDFilename);
	} else {
	    log("warning: reconfigure: unable to write new PID file `%s',"
		" reverting to old file `%s'", PIDFilename, old_PIDFilename);
	    free(PIDFilename);
	    PIDFilename = old_PIDFilename;
	    old_PIDFilename = NULL;
	}
    }

    /* For modules, we need to:
     *    - first unload any modules which don't have LoadModule lines
     *          anymore--this has to be done in reverse order to avoid
     *          dependency problems;
     *    - next reconfigure any still-loaded modules (because newly-loaded
     *          modules in the next step may depend on the new settings);
     *    - finally load any modules which weren't loaded before but have
     *          LoadModule lines now.
     */
    LoadModules_insert = LoadModules_count;
    for (i = old_LoadModules_count - 1; i >= 0; i--) {
	ARRAY_SEARCH_PLAIN(LoadModules, old_LoadModules[i], strcmp, j);
	if (j >= LoadModules_count) {
	    Module *mod = find_module(old_LoadModules[i]);
	    if (!mod) {
		log("BUG: reconfigure: module `%s' not available",
		    old_LoadModules[i]);
		retval = 0;
	    } else if (!unload_module(mod)) {
		log("warning: reconfigure: module `%s' could not be unloaded",
		    old_LoadModules[i]);
		ARRAY_INSERT(LoadModules, LoadModules_insert);
		LoadModules[LoadModules_insert] = sstrdup(old_LoadModules[i]);
		retval = 0;
	    }
	}
    }
    if (retval && !reconfigure_modules()) {
	log("warning: reconfigure: module reconfiguration failed");
	retval = 0;
    }
    if (retval) {
	ARRAY_FOREACH (i, LoadModules) {
	    ARRAY_SEARCH_PLAIN(old_LoadModules, LoadModules[i], strcmp, j);
	    if (j >= old_LoadModules_count) {
		if (!load_module(LoadModules[i])) {
		    log("warning: reconfigure: new module `%s' could not"
			" be loaded", LoadModules[i]);
		    ARRAY_REMOVE(LoadModules, i);
		    i--;
		    retval = 0;
		}
	    }
	}
    }

    /* Free old configuration data and return */
    free(old_RemoteServer);
    free(old_RemotePassword);
    free(old_LocalHost);
    free(old_ServerName);
    free(old_ServerDesc);
    free(old_ServiceUser);
    free(old_ServiceHost);
    free(old_LogFilename);
    free(old_PIDFilename);
    ARRAY_FOREACH (i, old_LoadModules)
	free(old_LoadModules[i]);
    free(old_LoadModules);
    return retval;
}

/*************************************************************************/
/******************************** Cleanup ********************************/
/*************************************************************************/

void cleanup(void)
{
    char lognamebuf[BUFSIZE], *lognamesave;
    int i;

    if (!*quitmsg)
	strscpy(quitmsg, "Terminating, reason unknown", sizeof(quitmsg));
    log("%s", quitmsg);
    set_cmode(NULL, NULL);
    unload_all_modules();
    if (servsock) {
	if (sock_isconn(servsock)) {
	    send_cmd(ServerName, "SQUIT %s :%s", ServerName, quitmsg);
	    disconn(servsock);
	}
	sock_free(servsock);
    }
    lang_cleanup();
    send_cleanup();
    actions_cleanup();
    messages_cleanup();
    process_cleanup();
    server_cleanup();
    channel_cleanup();
    user_cleanup();
    unregister_callback(NULL, cb_save_complete);
    unregister_callback(NULL, cb_save_data);
    unregister_callback(NULL, cb_connect);
    unregister_callback(NULL, cb_introduce_user);
    unregister_callback(NULL, cb_cmdline);
    modules_cleanup();
    /* Save a copy of LogFilename, because the log routines still need it */
    strscpy(lognamebuf, LogFilename, sizeof(lognamebuf));
    for (i = 0; strcmp(main_directives[i].name,"LogFilename") != 0; i++)
	;
    lognamesave = main_directives[i].params[0].prev.ptrval;
    main_directives[i].params[0].prev.ptrval = lognamebuf;
    deconfigure(main_directives);
    free(lognamesave);
    free(RemoteServer);
    RemoteServer = NULL;
    close_log();
    LogFilename = NULL;
}

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