/*
 * FCRON - periodic command scheduler 
 *
 *  Copyright 2000-2007 Thibault Godouet <fcron@free.fr>
 *
 *  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
 * 
 *  The GNU General Public License can also be found in the file
 *  `LICENSE' that comes with the fcron source distribution.
 */

 /* $Id: fcron.c,v 1.80 2007/04/14 18:04:14 thib Exp thib $ */

#include "fcron.h"

#include "database.h"
#include "conf.h"
#include "job.h"
#include "temp_file.h"
#ifdef FCRONDYN
#include "socket.h"
#endif

char rcs_info[] = "$Id: fcron.c,v 1.80 2007/04/14 18:04:14 thib Exp thib $";

void main_loop(void);
void check_signal(void);
void info(void);
void usage(void);
void print_schedule(void);
RETSIGTYPE sighup_handler(int x);
RETSIGTYPE sigterm_handler(int x);
RETSIGTYPE sigchild_handler(int x);
RETSIGTYPE sigusr1_handler(int x);
RETSIGTYPE sigusr2_handler(int x);
int parseopt(int argc, char *argv[]);
void get_lock(void);
void create_spooldir(char *dir);



/* command line options */
#ifdef DEBUG
char debug_opt = 1;       /* set to 1 if we are in debug mode */
#else
char debug_opt = 0;       /* set to 1 if we are in debug mode */
#endif

#ifdef FOREGROUND
char foreground = 1; /* set to 1 when we are on foreground, else 0 */
#else
char foreground = 0; /* set to 1 when we are on foreground, else 0 */
#endif

time_t first_sleep = FIRST_SLEEP;
time_t save_time = SAVE;
char once = 0;      /* set to 1 if fcron shall return immediately after running
		     * all jobs that are due at the time when fcron is started */
char dosyslog = 1;  /* set to 1 when we log messages to syslog, else 0 */

/* used in temp_file() : create it in current dir (normally spool dir) */
char *tmp_path = "";

/* process identity */
pid_t daemon_pid;
mode_t saved_umask;           /* default root umask */
char *prog_name = NULL;
char *orig_tz_envvar = NULL;

/* uid/gid of user/group root 
 * (we don't use the static UID or GID as we ask for user and group names
 * in the configure script) */
uid_t rootuid = 0;
gid_t rootgid = 0;

/* have we got a signal ? */
char sig_conf = 0;            /* is 1 when we got a SIGHUP, 2 for a SIGUSR1 */ 
char sig_chld = 0;            /* is 1 when we got a SIGCHLD */  
char sig_debug = 0;           /* is 1 when we got a SIGUSR2 */  

/* jobs database */
struct cf_t *file_base;         /* point to the first file of the list */
struct job_t *queue_base;       /* ordered list of normal jobs to be run */
unsigned long int next_id;    /* id for next line to enter database */

struct cl_t **serial_array;     /* ordered list of job to be run one by one */
short int serial_array_size;  /* size of serial_array */
short int serial_array_index; /* the index of the first job */
short int serial_num;         /* number of job being queued */
short int serial_running;     /* number of running serial jobs */

/* do not run more than this number of serial job simultaneously */
short int serial_max_running = SERIAL_MAX_RUNNING; 
short int serial_queue_max   = SERIAL_QUEUE_MAX;
short int lavg_queue_max     = LAVG_QUEUE_MAX;

struct lavg_t *lavg_array;      /* jobs waiting for a given system load value */
short int lavg_array_size;    /* size of lavg_array */
short int lavg_num;           /* number of job being queued */
short int lavg_serial_running;/* number of serialized lavg job being running */

struct exe_t *exe_array;        /* jobs which are executed */
short int exe_array_size;     /* size of exe_array */
short int exe_num;            /* number of job being executed */

time_t begin_sleep;           /* the time at which sleep began */
time_t now;                   /* the current time */

#ifdef HAVE_LIBPAM
pam_handle_t *pamh = NULL;
const struct pam_conv apamconv = { NULL };
#endif

void
info(void)
    /* print some informations about this program :
     * version, license */
{
    fprintf(stderr,
	    "fcron " VERSION_QUOTED " - periodic command scheduler\n"
	    "Copyright " COPYRIGHT_QUOTED " Thibault Godouet <fcron@free.fr>\n"
	    "This program is free software distributed WITHOUT ANY WARRANTY.\n"
            "See the GNU General Public License for more details.\n"
	);

    exit(EXIT_OK);

}


void
usage(void)
  /*  print a help message about command line options and exit */
{
    fprintf(stderr, "\nfcron " VERSION_QUOTED "\n\n"
	    "fcron [-d] [-f] [-b]\n"
	    "fcron -h\n"
	    "  -s t   --savetime t     Save fcrontabs on disk every t sec.\n"
	    "  -l t   --firstsleep t   Sets the initial delay before any job is executed"
	    ",\n                          default to %d seconds.\n"
	    "  -m n   --maxserial n    Set to n the max number of running serial jobs.\n"
	    "  -c f   --configfile f   Make fcron use config file f.\n"
	    "  -n d   --newspooldir d  Create d as a new spool directory.\n"
	    "  -f     --foreground     Stay in foreground.\n"
	    "  -b     --background     Go to background.\n"
	    "  -y     --nosyslog       Don't log to syslog at all.\n"
	    "  -o     --once           Execute all jobs that need to be run, wait for "
	    "them,\n                          then return. Sets firstsleep to 0.\n"
	    "                          Especially useful with -f and -y.\n"
	    "  -d     --debug          Set Debug mode.\n"
	    "  -h     --help           Show this help message.\n"
	    "  -V     --version        Display version & infos about fcron.\n",
	    FIRST_SLEEP
	);
    
    exit(EXIT_ERR);
}


void
print_schedule(void)
    /* print the current schedule on syslog */
{
    cf_t *cf;
    cl_t *cl;
    struct tm *ftime;

    explain("Printing schedule ...");
    for (cf = file_base; cf; cf = cf->cf_next) {
	explain(" File %s", cf->cf_user);
	for (cl = cf->cf_line_base; cl; cl = cl->cl_next) {
	    ftime = localtime( &(cl->cl_nextexe) );
	    explain("  cmd %s next exec %d/%d/%d wday:%d %02d:%02d"
		    " (system time)",
		    cl->cl_shell, (ftime->tm_mon + 1), ftime->tm_mday,
		    (ftime->tm_year + 1900), ftime->tm_wday,
		    ftime->tm_hour, ftime->tm_min); 
	    
	}
    }
    explain("... end of printing schedule.");
}


void 
xexit(int exit_value) 
    /* exit after having freed memory and removed lock file */
{
    cf_t *f = NULL;

    now = time(NULL);

    /* we save all files now and after having waiting for all
     * job being executed because we might get a SIGKILL
     * if we don't exit quickly */
    save_file(NULL);
    
#ifdef FCRONDYN
    close_socket();
#endif

    f = file_base;
    while ( f != NULL ) {
	if ( f->cf_running > 0 ) {
	    /* */
	    debug("waiting jobs for %s ...", f->cf_user);
	    /* */
	    wait_all( &f->cf_running );	    
	    save_file(f);
	}
	delete_file(f->cf_user);    

	/* delete_file remove the f file from the list :
	 * next file to remove is now pointed by file_base. */
	f = file_base;
    }

    remove(pidfile);
    
    free_conf();

    free(orig_tz_envvar);

    explain("Exiting with code %d", exit_value);
    exit (exit_value);

}

void
get_lock()
    /* check if another fcron daemon is running with the same config (-c option) :
     * in this case, die. if not, write our pid to /var/run/fcron.pid in order to lock,
     * and to permit fcrontab to read our pid and signal us */
{
    int otherpid = 0;
    FILE *daemon_lockfp = NULL;
    int fd;

    if (((fd = open(pidfile, O_RDWR|O_CREAT, 0644)) == -1 )
	|| ((daemon_lockfp = fdopen(fd, "r+"))) == NULL)
	die_e("can't open or create %s", pidfile);
	
#ifdef HAVE_FLOCK
    if ( flock(fd, LOCK_EX|LOCK_NB) != 0 )
#else /* HAVE_FLOCK */
	if ( lockf(fileno(daemon_lockfp), F_TLOCK, 0) != 0 )
#endif /* ! HAVE_FLOCK */
	    {
		if ( fscanf(daemon_lockfp, "%d", &otherpid) >= 1 )
		    die_e("can't lock %s, running daemon's pid may be %d",
			  pidfile, otherpid);
		else
		    die_e("can't lock %s, and unable to read running"
			  " daemon's pid", pidfile);
	    }

    fcntl(fd, F_SETFD, 1);

    rewind(daemon_lockfp);
    fprintf(daemon_lockfp, "%d\n", (int) daemon_pid);
    fflush(daemon_lockfp);
    if ( ftruncate(fileno(daemon_lockfp), ftell(daemon_lockfp)) < 0 )
	error_e("Unable to ftruncate(fileno(daemon_lockfp), ftell(daemon_lockfp))");

    /* abandon fd and daemon_lockfp even though the file is open. we need to
     * keep it open and locked, but we don't need the handles elsewhere.
     */

}


int
parseopt(int argc, char *argv[])
  /* set options */
{

    int c;
    int i;

#ifdef HAVE_GETOPT_LONG
    static struct option opt[] =
    {
	{"debug", 0, NULL, 'd'},
	{"foreground", 0, NULL, 'f'},
	{"background", 0, NULL, 'b'},
 	{"nosyslog", 0, NULL, 'y'},
	{"help", 0, NULL, 'h'},
	{"version", 0, NULL, 'V'},
 	{"once", 0, NULL, 'o'},
	{"savetime", 1, NULL, 's'},
 	{"firstsleep", 1, NULL, 'l'},
	{"maxserial", 1, NULL, 'm'},
	{"configfile", 1, NULL, 'c'},
	{"newspooldir", 1, NULL, 'n'},
	{"queuelen", 1, NULL, 'q'},
	{0,0,0,0}
    };
#endif /* HAVE_GETOPT_LONG */

    extern char *optarg;
    extern int optind, opterr, optopt;

    /* constants and variables defined by command line */

    while(1) {
#ifdef HAVE_GETOPT_LONG
	c = getopt_long(argc, argv, "dfbyhVos:l:m:c:n:q:", opt, NULL);
#else
	c = getopt(argc, argv, "dfbyhVos:l:m:c:n:q:");
#endif /* HAVE_GETOPT_LONG */
	if ( c == EOF ) break;
	switch ( (char)c ) {

	case 'V':
	    info(); break;

	case 'h':
	    usage(); break;

	case 'd':
	    debug_opt = 1; break;

	case 'f':
	    foreground = 1; break;

	case 'b':
	    foreground = 0; break;

 	case 'y':
	    dosyslog = 0; break;
	    
 	case 'o':
	    once = 1; first_sleep = 0; break;

	case 's':
	    if ( (save_time = strtol(optarg, NULL, 10)) < 60 || save_time >= LONG_MAX )
		die("Save time can only be set between 60 and %d.", LONG_MAX); 
	    break;

 	case 'l':
	    if ( (first_sleep = strtol(optarg, NULL, 10)) < 0 || first_sleep >= LONG_MAX)
		die("First sleep can only be set between 0 and %d.", LONG_MAX); 
	    break;
	    
	case 'm':
	    if ( (serial_max_running = strtol(optarg, NULL, 10)) <= 0 
		 || serial_max_running >= SHRT_MAX )
		die("Max running can only be set between 1 and %d.",SHRT_MAX);
	    break;

	case 'c':
	    Set(fcronconf, optarg);
	    break;

	case 'n':
	    create_spooldir(optarg);
	    break;

	case 'q':
	    if ( (lavg_queue_max = serial_queue_max = strtol(optarg, NULL, 10)) < 5 
		|| serial_queue_max >= SHRT_MAX )
		die("Queue length can only be set between 5 and %d.", SHRT_MAX);
	    break;

	case ':':
	    error("(parseopt) Missing parameter");
	    usage();

	case '?':
	    usage();

	default:
	    warn("(parseopt) Warning: getopt returned %c", c);
	}
    }

    if (optind < argc) {
	for (i = optind; i<=argc; i++)
	    error("Unknown argument \"%s\"", argv[i]);
	usage();
    }

    return OK;

}

void 
create_spooldir(char *dir)
    /* create a new spool dir for fcron : set correctly its mode and owner */
{
    int dir_fd = -1;
    struct stat st;
    uid_t useruid = get_user_uid_safe(USERNAME);
    gid_t usergid = get_group_gid_safe(GROUPNAME);

    if ( mkdir(dir, 0) != 0 && errno != EEXIST )
	die_e("Cannot create dir %s", dir);

    if ( (dir_fd = open(dir, 0)) < 0 )
	die_e("Cannot open dir %s", dir);

    if ( fstat(dir_fd, &st) != 0 ) {
	close(dir_fd);
	die_e("Cannot fstat %s", dir);
    }

    if ( ! S_ISDIR(st.st_mode) ) {
	close(dir_fd);
	die("%s exists and is not a directory", dir);
    }

    if ( fchown(dir_fd, useruid, usergid) != 0 ) {
	close(dir_fd);
	die_e("Cannot fchown dir %s to %s:%s", dir, USERNAME, GROUPNAME);
    }

    if (fchmod(dir_fd, S_IRUSR|S_IWUSR|S_IXUSR|S_IRGRP|S_IWGRP|S_IXGRP) != 0) {
	close(dir_fd);
	die_e("Cannot change dir %s's mode to 770", dir);
    }

    close(dir_fd);

    exit(EXIT_OK);

}


RETSIGTYPE
sigterm_handler(int x)
  /* exit safely */
{
    debug("");
    explain("SIGTERM signal received");
    xexit(EXIT_OK);
}

RETSIGTYPE
sighup_handler(int x)
  /* update configuration */
{
    /* we don't call the synchronize_dir() function directly,
       because it may cause some problems if this signal
       is not received during the sleep
    */
    sig_conf = 1;
}

RETSIGTYPE
sigchild_handler(int x)
  /* call wait_chld() to take care of finished jobs */
{
    
    sig_chld = 1;

}


RETSIGTYPE
sigusr1_handler(int x)
  /* reload all configurations */
{
    /* we don't call the synchronize_dir() function directly,
       because it may cause some problems if this signal
       is not received during the sleep
    */
    sig_conf = 2;
}


RETSIGTYPE
sigusr2_handler(int x)
  /* print schedule and switch on/off debug mode */
{
    sig_debug = 1;
}


int
main(int argc, char **argv)
{

    rootuid = get_user_uid_safe(ROOTNAME);
    rootgid = get_group_gid_safe(ROOTGROUP);

    /* we set it to 022 in order to get a pidfile readable by fcrontab
     * (will be set to 066 later) */
    saved_umask = umask(022);

    /* parse options */

    if ( strrchr(argv[0], '/') == NULL) prog_name = argv[0];
    else prog_name = strrchr(argv[0], '/') + 1;

    {
	uid_t daemon_uid;                 
	if ( (daemon_uid = getuid()) != rootuid )
	    die("Fcron must be executed as root");
    }

    /* we have to set daemon_pid before the fork because it's
     * used in die() and die_e() functions */
    daemon_pid = getpid();

    /* save the value of the TZ env variable (used for option timezone) */
    orig_tz_envvar = strdup2(getenv("TZ"));

    parseopt(argc, argv);

    /* read fcron.conf and update global parameters */
    read_conf();

    /* change directory */

    if (chdir(fcrontabs) != 0)
	die_e("Could not change dir to %s", fcrontabs);
    

    freopen("/dev/null", "r", stdin);

    if (foreground == 0) {

	/* close stdout and stderr.
	 * close unused descriptors
	 * optional detach from controlling terminal */

	int fd;
	pid_t pid;

	switch ( pid = fork() ) {
	case -1:
	    die_e("fork");
	    break;
	case 0:
	    /* child */
	    break;
	default:
	    /* parent */
/*  	    printf("%s[%d] " VERSION_QUOTED " : started.\n", */
/*  		   prog_name, pid); */
	    exit(0);
	}

	daemon_pid = getpid();

	if ((fd = open("/dev/tty", O_RDWR)) >= 0) {
#ifndef _HPUX_SOURCE
	    ioctl(fd, TIOCNOTTY, 0);
#endif
	    close(fd);
	}
	
	freopen("/dev/null", "w", stdout);
	freopen("/dev/null", "w", stderr);

	/* close most other open fds */
	xcloselog();
	for(fd = 3; fd < 250; fd++) (void) close(fd);

	/* finally, create a new session */
	if ( setsid() == -1 )
	    error("Could not setsid()");

    }

    /* check if another fcron daemon is running, create pid file and lock it */
    get_lock();
    
    /* this program belongs to root : we set default permission mode
     * to  600 for security reasons, but we reset them to the saved
     * umask just before we run a job */
    umask(066);

    explain("%s[%d] " VERSION_QUOTED " started", prog_name, daemon_pid);

#ifdef HAVE_SIGNAL
    signal(SIGTERM, sigterm_handler);
    signal(SIGHUP, sighup_handler);
    siginterrupt(SIGHUP, 0);
    signal(SIGCHLD, sigchild_handler);
    siginterrupt(SIGCHLD, 0);
    signal(SIGUSR1, sigusr1_handler);
    siginterrupt(SIGUSR1, 0);
    signal(SIGUSR2, sigusr2_handler);
    siginterrupt(SIGUSR2, 0);
    /* we don't want SIGPIPE to kill fcron, and don't need to handle it */
    signal(SIGPIPE, SIG_IGN);
#elif HAVE_SIGSET
    sigset(SIGTERM, sigterm_handler);
    sigset(SIGHUP, sighup_handler);
    sigset(SIGCHLD, sigchild_handler);
    sigset(SIGUSR1, sigusr1_handler);
    sigset(SIGUSR2, sigusr2_handler);
    sigset(SIGPIPE, SIG_IGN);
#endif

    /* initialize job database */
    next_id = 0;

    /* initialize exe_array */
    exe_num = 0;
    exe_array_size = EXE_INITIAL_SIZE;
    if ( (exe_array = calloc(exe_array_size, sizeof(struct exe_t))) == NULL )
	die_e("could not calloc exe_array");

    /* initialize serial_array */
    serial_running = 0;
    serial_array_index = 0;
    serial_num = 0;
    serial_array_size = SERIAL_INITIAL_SIZE;
    if ( (serial_array = calloc(serial_array_size, sizeof(cl_t *))) == NULL )
	die_e("could not calloc serial_array");

    /* initialize lavg_array */
    lavg_num = 0;
    lavg_serial_running = 0;
    lavg_array_size = LAVG_INITIAL_SIZE;
    if ( (lavg_array = calloc(lavg_array_size, sizeof(lavg_t))) == NULL )
	die_e("could not calloc lavg_array");

#ifdef FCRONDYN
    /* initialize socket */
    init_socket();
#endif

    /* initialize random number generator :
     * WARNING : easy to guess !!! */
    srand(time(NULL));

    main_loop();

    /* never reached */
    return EXIT_OK;
}


void
check_signal()
    /* check if a signal has been received and handle it */
{

    /* we reinstall the signal handler functions here and not directly in the handlers,
     * as it is not supported on some systems (HP-UX) and makes fcron crash */

    if (sig_chld > 0) {
	wait_chld();
	sig_chld = 0;
#ifdef HAVE_SIGNAL
	(void)signal(SIGCHLD, sigchild_handler);
	siginterrupt(SIGCHLD, 0);
#endif
    }
    if (sig_conf > 0) {

	if (sig_conf == 1) {
	    /* update configuration */
	    synchronize_dir(".");
	    sig_conf = 0;
#ifdef HAVE_SIGNAL
	    signal(SIGHUP, sighup_handler);
	    siginterrupt(SIGHUP, 0);
#endif
	}
	else {
	    /* reload all configuration */
	    reload_all(".");
	    sig_conf = 0;
#ifdef HAVE_SIGNAL
	    signal(SIGUSR1, sigusr1_handler);
	    siginterrupt(SIGUSR1, 0);
#endif
	}

    }
    if (sig_debug > 0) {
	print_schedule();
	debug_opt = (debug_opt > 0)? 0 : 1;
	explain("debug_opt = %d", debug_opt);
	sig_debug = 0;
#ifdef HAVE_SIGNAL
	signal(SIGUSR2, sigusr2_handler);
	siginterrupt(SIGUSR2, 0);
#endif
    }

}

void
main_loop()
  /* main loop - get the time to sleep until next job execution,
   *             sleep, and then test all jobs and execute if needed. */
{
    time_t save;           /* time remaining until next save */
    time_t stime;          /* time to sleep until next job
			    * execution */
#ifdef HAVE_GETTIMEOFDAY
    struct timeval tv;     /* we use usec field to get more precision */
#endif
#ifdef FCRONDYN
    int retcode = 0;
#endif

    debug("Entering main loop");

    now = time(NULL);

    synchronize_dir(".");

    /* synchronize save with jobs execution */
    save = now + save_time;

    if ( serial_num > 0 || once )
	stime = first_sleep;
    else if ( (stime = time_to_sleep(save)) < first_sleep )
	/* force first execution after first_sleep sec : execution of jobs
	 * during system boot time is not what we want */
	stime = first_sleep;

    for (;;) {
	
#ifdef HAVE_GETTIMEOFDAY
#ifdef FCRONDYN
	gettimeofday(&tv, NULL);
	tv.tv_sec = (stime > 1) ? stime - 1 : 0;
	/* we set tv_usec to slightly more than necessary so as
	 * we don't wake up too early, in which case we would
	 * have to sleep again for some time */
	tv.tv_usec = 1001000 - tv.tv_usec;
	/* On some systems (BSD, etc), tv_usec cannot be greater than 999999 */
	if (tv.tv_usec > 999999)
	    tv.tv_usec = 999999;
	/* note: read_set is set in socket.c */
	if((retcode = select(set_max_fd+1, &read_set, NULL, NULL, &tv)) < 0 && errno != EINTR)
	    die_e("select returned %d", errno);
#else
	if (stime > 1)
	    sleep(stime - 1);
	gettimeofday(&tv, NULL);
	/* we set tv_usec to slightly more than necessary to avoid 
	 * infinite loop */
	usleep( 1001000 - tv.tv_usec );
#endif /* FCRONDYN */
#else
	sleep(stime);
#endif /* HAVE_GETTIMEOFDAY */

	now = time(NULL);

	check_signal();

	debug("\n");
	test_jobs();

    	while ( serial_num > 0 && serial_running < serial_max_running )
     	    run_serial_job();

 	if ( once ) {
	    explain("Running with option once : exiting ... ");
	    xexit(EXIT_OK);
	}

	if ( save <= now ) {
	    save = now + save_time;
	    /* save all files */
	    save_file(NULL);
	}

#ifdef FCRONDYN
	/* check if there's a new connection, a new command to answer, etc ... */
	/* we do that *after* other checks, to avoid Denial Of Service attacks */
	check_socket(retcode);
#endif

	stime = check_lavg(save);
	debug("next sleep time : %ld", stime);

	check_signal();

    }

}


syntax highlighted by Code2HTML, v. 0.9.1