/*
 * FCRON - periodic command scheduler 
 *
 *  Copyright 2000-2024 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.
 */


#include "fcron.h"

#include "database.h"
#include "conf.h"
#include "job.h"
#include "temp_file.h"
#include "fcronconf.h"
#include "select.h"
#include "suspend.h"
#ifdef FCRONDYN
#include "fcrondyn_svr.h"
#endif


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

/* command line options */

#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;
const time_t min_sleep_usec = 1000;     /* 1ms -- we won't sleep for less than this time */
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 */

/* Get the default locale character set for the mail
 * "Content-Type: ...; charset=" header */
char default_mail_charset[TERM_LEN] = "";

/* 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 */
char sig_cont = 0;              /* is 1 when we got a SIGCONT */

/* 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;

lavg_list_t *lavg_list;         /* jobs waiting for a given system load value */
short int lavg_queue_max = LAVG_QUEUE_MAX;
short int lavg_serial_running;  /* number of serialized lavg job being running */

exe_list_t *exe_list;           /* jobs which are executed */

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"
            "  -p     --logfilepath    If set, log to the file given as argument.\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 %04d-%02d-%02d wday:%d %02d:%02d"
                    " (system time)",
                    cl->cl_shell, (ftime->tm_year + 1900), (ftime->tm_mon + 1),
                    ftime->tm_mday, 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 = my_time();

    /* 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
    fcrondyn_socket_close(NULL);
#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);

    exe_list_destroy(exe_list);
    lavg_list_destroy(lavg_list);
    free_conf();

    Free_safe(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
is_system_startup(void)
{
    int reboot = 0;

    /* lock exist - skip reboot jobs */
    if (access(REBOOT_LOCK, F_OK) == 0) {
        explain("@reboot jobs will only be run at computer's startup.");
        /* don't run @reboot jobs */
        return 0;
    }
    /* lock doesn't exist - create lock, run reboot jobs */
    if ((reboot = creat(REBOOT_LOCK, S_IRUSR & S_IWUSR)) < 0)
        error_e("Can't create lock for reboot jobs.");
    else
        xclose_check(&reboot, REBOOT_LOCK);

    /* run @reboot jobs */
    return 1;
}


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'},
        {"logfilepath", 1, NULL, 'p'},
        {"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, "dfbyp:hVos:l:m:c:n:q:", opt, NULL);
#else
        c = getopt(argc, argv, "dfbyp:hVos: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 'p':
            logfile_path = strdup2(optarg);
            break;

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

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

        case 'l':
            if ((first_sleep = strtol(optarg, NULL, 10)) < 0
                || first_sleep >= TIME_T_MAX)
                die("First sleep can only be set between 0 and %d.",
                    TIME_T_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) {
        xclose_check(&dir_fd, "spooldir");
        die_e("Cannot fstat %s", dir);
    }

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

    if (fchown(dir_fd, useruid, usergid) != 0) {
        xclose_check(&dir_fd, "spooldir");
        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) {
        xclose_check(&dir_fd, "spooldir");
        die_e("Cannot change dir %s's mode to 770", dir);
    }

    xclose_check(&dir_fd, "spooldir");

    exit(EXIT_OK);

}


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

void
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;
}

void
sigchild_handler(int x)
  /* call wait_chld() to take care of finished jobs */
{

    sig_chld = 1;

}


void
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;
}


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

void
sigcont_handler(int x)
  /* used to notify fcron of a system resume after suspend.
   * However this signal could also be received in other cases. */
{
    sig_cont = 1;
}

int
main(int argc, char **argv)
{
    char *codeset = NULL;

    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();

    /* initialize the logs before we become a daemon */
    xopenlog();

    /* change directory */

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

    /* Get the default locale character set for the mail
     * "Content-Type: ...; charset=" header */
    setlocale(LC_ALL, "");      /* set locale to system defaults or to
                                 * that specified by any  LC_* env vars */
    /* Except that "US-ASCII" is preferred to "ANSI_x3.4-1968" in MIME,
     * even though "ANSI_x3.4-1968" is the official charset name. */
    if ((codeset = nl_langinfo(CODESET)) != 0L &&
        strcmp(codeset, "ANSI_x3.4-1968") != 0)
        strncpy(default_mail_charset, codeset, sizeof(default_mail_charset));
    else
        strcpy(default_mail_charset, "US-ASCII");

    if (freopen("/dev/null", "r", stdin) == NULL)
        error_e("Could not open /dev/null as 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
            xclose_check(&fd, "/dev/tty");
        }

        if (freopen("/dev/null", "w", stdout) == NULL)
            error_e("Could not open /dev/null as stdout");
        if (freopen("/dev/null", "w", stderr) == NULL)
            error_e("Could not open /dev/null as stderr");

        /* close most other open fds */
        xcloselog();
        for (fd = 3; fd < 250; fd++)
            /* don't use xclose_check() as we do expect most of them to fail */
            (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
    /* FIXME: check for errors */
    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);
    signal(SIGCONT, sigcont_handler);
    siginterrupt(SIGCONT, 0);
    /* we don't want SIGPIPE to kill fcron, and don't need to handle it as when ignored
     * write() on a pipe closed at the other end will return EPIPE */
    signal(SIGPIPE, SIG_IGN);
#elif HAVE_SIGSET
    /* FIXME: check for errors */
    sigset(SIGTERM, sigterm_handler);
    sigset(SIGHUP, sighup_handler);
    sigset(SIGCHLD, sigchild_handler);
    sigset(SIGUSR1, sigusr1_handler);
    sigset(SIGUSR2, sigusr2_handler);
    sigset(SIGCONT, sigcont_handler);
    sigset(SIGPIPE, SIG_IGN);
#endif

    /* initialize job database */
    next_id = 0;

    /* initialize exe_array */
    exe_list = exe_list_init();

    /* initialize serial_array */
    serial_running = 0;
    serial_array_index = 0;
    serial_num = 0;
    serial_array_size = SERIAL_INITIAL_SIZE;
    serial_array =
        alloc_safe(serial_array_size * sizeof(cl_t *), "serial_array");

    /* initialize lavg_array */
    lavg_list = lavg_list_init();
    lavg_list->max_entries = lavg_queue_max;
    lavg_serial_running = 0;

    /* initialize random number generator :
     * WARNING : easy to guess !!! */
    /* we use the hostname and tv_usec in order to get different seeds
     * on two different machines starting fcron at the same moment */
    {
        char hostname[50];
        int i;
        unsigned int seed;
#ifdef HAVE_GETTIMEOFDAY
        struct timeval tv;      /* we use usec field to get more precision */
        gettimeofday(&tv, NULL);
        seed = ((unsigned int)tv.tv_usec) ^ ((unsigned int)tv.tv_sec);
#else
        seed = (unsigned int)time(NULL);
#endif
        gethostname(hostname, sizeof(hostname));

        for (i = 0; i < sizeof(hostname) - sizeof(seed); i += sizeof(seed))
            seed ^= (unsigned int)*(hostname + i);

        srand(seed);
    }

    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(".", 0);
            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
reset_sig_cont(void)
   /* reinitialize the SIGCONT handler if it was raised */
{
    if (sig_cont > 0) {

        sig_cont = 0;
#ifdef HAVE_SIGNAL
        signal(SIGCONT, sigcont_handler);
        siginterrupt(SIGCONT, 0);
#endif
    }
}

#ifdef HAVE_GETTIMEOFDAY
#define debug_print_tstamp(where_str) \
        { \
        if (debug_opt) { \
            gettimeofday(&now_tv, NULL); \
            debug(where_str ": now=%ld now_tv sec=%ld usec=%ld", now, now_tv.tv_sec, \
                  now_tv.tv_usec); \
        } \
    }
#else
#define debug_print_tstamp(where_str) { ; }
#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 slept_from;          /* time it was when we went into sleep */
    time_t nwt;                 /* next wake time */
#ifdef HAVE_GETTIMEOFDAY
    struct select_instance main_select;
    struct timeval now_tv;      /* we use usec field to get more precision */
    struct timeval sleep_tv;    /* we use usec field to get more precision */
#endif

    debug("Entering main loop");

#ifdef HAVE_GETTIMEOFDAY
    select_init(&main_select);
#ifdef FCRONDYN
    fcrondyn_socket_init(&main_select);
#endif
    init_suspend(&main_select);
#endif

    now = my_time();

    synchronize_dir(".", is_system_startup());

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

    if (serial_num > 0 || once) {
        nwt = now + first_sleep;
    }
    else {
        /* force first execution after first_sleep sec : execution of jobs
         * during system boot time is not what we want */
        nwt = tmax(next_wake_time(save), now + first_sleep);
    }
    debug("initial wake time : %s", ctime(&nwt));

    for (;;) {

        /* remember when we started to sleep -- this is to detect suspend/hibernate */
        slept_from = time(NULL);

#ifdef HAVE_GETTIMEOFDAY
        gettimeofday(&now_tv, NULL);
        debug("now gettimeofday tv_sec=%ld, tv_usec=%ld %s", now_tv.tv_sec,
              now_tv.tv_usec, ctime(&nwt));

        if (nwt <= now_tv.tv_sec) {
            sleep_tv.tv_sec = 0;
            sleep_tv.tv_usec = 0;
        }
        else {
            /* compensate for any time spent in the loop,
             * so as we wake up exactly at the beginning of the second */
            sleep_tv.tv_sec = nwt - now_tv.tv_sec - 1;
            /* we set tv_usec to slightly more than necessary so as we don't wake
             * up too early (e.g. due to rounding to the system clock granularity),
             * in which case we would have to go back to sleep again */
            sleep_tv.tv_usec = 1000000 + min_sleep_usec - now_tv.tv_usec;
        }

        if (sleep_tv.tv_usec > 999999) {
            /* On some systems (BSD, etc), tv_usec cannot be greater than 999999 */
            sleep_tv.tv_usec = 999999;
        }
        else if (sleep_tv.tv_sec == 0 && sleep_tv.tv_usec < min_sleep_usec) {
            /* to prevent any infinite loop, sleep at least 1ms */
            debug
                ("We'll sleep for a tiny bit to avoid any risk of infinite loop");
            sleep_tv.tv_usec = min_sleep_usec;
        }
        debug("nwt=%s, sleep sec=%ld, usec=%ld", ctime(&nwt), sleep_tv.tv_sec,
              sleep_tv.tv_usec);
        select_call(&main_select, &sleep_tv);
#else                           /* HAVE_GETTIMEOFDAY */
        if (nwt - now > 0) {
            sleep(nwt - now);
        }
#endif                          /* HAVE_GETTIMEOFDAY */

        debug("\n");
        now = my_time();
        debug_print_tstamp("just woke up");

        check_signal();
        check_suspend(slept_from, nwt, &sig_cont, &main_select);
        reset_sig_cont();
        debug_print_tstamp("after check_signal and suspend");

        test_jobs();
        debug_print_tstamp("after 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);
        }
        debug_print_tstamp("after save");
#if defined(FCRONDYN) && defined(HAVE_GETTIMEOFDAY)
        /* 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 */
        fcrondyn_socket_check(&main_select);
#endif

        nwt = check_lavg(save);
        debug_print_tstamp("after check_lavg");
        debug("next wake time : %s", ctime(&nwt));

        check_signal();

    }

}
