/* * atd.c - run jobs queued by at; run with root privileges. * Copyright (C) 1993, 1994, 1996 Thomas Koenig * Copyright (c) 2002, 2005 Ryan Murray * * 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #ifdef HAVE_CONFIG_H #include "config.h" #endif /* System Headers */ #include #include #ifdef HAVE_SYS_WAIT_H #include #endif #ifdef HAVE_FCNTL_H #include #elif HAVE_SYS_FCNTL_H #include #endif #include #ifdef HAVE_DIRENT_H #include #elif HAVE_SYS_DIRENT_H #include #elif HAVE_SYS_DIR_H #include #endif #ifdef HAVE_ERRNO_H #include #endif #include #include #include #include #include #include #include #include #ifdef HAVE_UNISTD_H #include #endif #ifdef HAVE_GETOPT_H #include #endif #ifdef HAVE_UNISTD_H #include #endif #include #include /* Local headers */ #include "privs.h" #include "daemon.h" #ifndef HAVE_GETLOADAVG #include "getloadavg.h" #endif #ifdef WITH_SELINUX #include #include int selinux_enabled = 0; #endif /* Macros */ #ifndef LOG_ATD #define LOG_ATD LOG_DAEMON #endif #define BATCH_INTERVAL_DEFAULT 60 #define CHECK_INTERVAL 3600 #ifndef MAXHOSTNAMELEN #define MAXHOSTNAMELEN 64 #endif /* Global variables */ uid_t real_uid, effective_uid; gid_t real_gid, effective_gid; uid_t daemon_uid = (uid_t) - 3; gid_t daemon_gid = (gid_t) - 3; /* File scope variables */ static char *namep; static double load_avg = LOADAVG_MX; static time_t now; static time_t last_chg; static int nothing_to_do; unsigned int batch_interval; static int run_as_daemon = 0; static int mail_with_hostname = 0; static volatile sig_atomic_t term_signal = 0; #ifdef WITH_PAM #include static pam_handle_t *pamh = NULL; static const struct pam_conv conv = { NULL }; #endif /* WITH_PAM */ /* Signal handlers */ RETSIGTYPE set_term(int dummy) { term_signal = 1; return; } RETSIGTYPE sdummy(int dummy) { /* Empty signal handler */ nothing_to_do = 0; return; } /* SIGCHLD handler - discards completion status of children */ RETSIGTYPE release_zombie(int dummy) { int status; pid_t pid; while ((pid = waitpid(-1, &status, WNOHANG)) > 0) { #ifdef DEBUG_ZOMBIE if (WIFEXITED(status)) syslog(LOG_INFO, "pid %ld exited with status %d.", pid, WEXITSTATUS(status)); else if (WIFSIGNALED(status)) syslog(LOG_NOTICE, "pid %ld killed with signal %d.", pid, WTERMSIG(status)); else if (WIFSTOPPED(status)) syslog(LOG_NOTICE, "pid %ld stopped with signal %d.", pid, WSTOPSIG(status)); else syslog(LOG_WARNING, "pid %ld unknown reason for SIGCHLD", pid); #endif } return; } /* Local functions */ static int write_string(int fd, const char *a) { return write(fd, a, strlen(a)); } static int isbatch(char queue) { return isupper(queue) || (queue == 'b'); } #undef DEBUG_FORK #ifdef DEBUG_FORK static pid_t myfork() { pid_t res; res = fork(); if (res == 0) kill(getpid(), SIGSTOP); return res; } #define fork myfork #endif #undef ATD_MAIL_PROGRAM #undef ATD_MAIL_NAME #if defined(SENDMAIL) #define ATD_MAIL_PROGRAM SENDMAIL #define ATD_MAIL_NAME "sendmail" #elif defined(MAILC) #define ATD_MAIL_PROGRAM MAILC #define ATD_MAIL_NAME "mail" #elif defined(MAILX) #define ATD_MAIL_PROGRAM MAILX #define ATD_MAIL_NAME "mailx" #endif #ifdef WITH_SELINUX static int set_selinux_context(const char *name, const char *filename) { security_context_t user_context = NULL; security_context_t file_context = NULL; int retval = 0; char *seuser = NULL; char *level = NULL; if (getseuserbyname(name, &seuser, &level) == 0) { retval = get_default_context_with_level(seuser, level, NULL, &user_context); free(seuser); free(level); if (retval < 0) { lerr("get_default_context_with_level: couldn't get security context for user %s", name); retval = -1; goto err; } } /* * Since crontab files are not directly executed, * crond must ensure that the crontab file has * a context that is appropriate for the context of * the user cron job. It performs an entrypoint * permission check for this purpose. */ if (fgetfilecon(STDIN_FILENO, &file_context) < 0) { lerr("fgetfilecon FAILED %s", filename); retval = -1; goto err; } retval = selinux_check_access(user_context, file_context, "file", "entrypoint", NULL); freecon(file_context); if (retval < 0) { lerr("Not allowed to set exec context to %s for user %s", user_context, name); retval = -1; goto err; } if (setexeccon(user_context) < 0) { lerr("Could not set exec context to %s for user %s", user_context, name); retval = -1; goto err; } err: if (retval < 0 && security_getenforce() != 1) retval = 0; if (user_context) freecon(user_context); return retval; } static int selinux_log_callback (int type, const char *fmt, ...) { va_list ap; va_start(ap, fmt); vsyslog (LOG_ERR, fmt, ap); va_end(ap); return 0; } #endif static void run_file(const char *filename, uid_t uid, gid_t gid) { /* Run a file by by spawning off a process which redirects I/O, * spawns a subshell, then waits for it to complete and sends * mail to the user. */ pid_t pid; int fd_out, fd_in, fd_std; char jobbuf[9]; char *mailname = NULL; int mailsize = 128; char *newname; FILE *stream; int send_mail = 0; struct stat buf, lbuf; off_t size; struct passwd *pentry; int fflags; int nuid; int ngid; char queue; char fmt[64]; unsigned long jobno; int rc; char hostbuf[MAXHOSTNAMELEN]; #ifdef WITH_PAM int retcode; #endif #ifdef _SC_LOGIN_NAME_MAX errno = 0; rc = sysconf(_SC_LOGIN_NAME_MAX); if (rc > 0) mailsize = rc; #else # ifdef LOGIN_NAME_MAX mailsize = LOGIN_NAME_MAX; # endif #endif sscanf(filename, "%c%5lx", &queue, &jobno); if ((mailname = malloc(mailsize+1)) == NULL) pabort("Job %8lu : out of virtual memory", jobno); sprintf(jobbuf, "%8lu", jobno); if ((newname = strdup(filename)) == 0) pabort("Job %8lu : out of virtual memory", jobno); newname[0] = '='; /* We try to make a hard link to lock the file. If we fail, then * somebody else has already locked or deleted it (a second atd?); log the * fact and return. */ PRIV_START rc = link(filename, newname); PRIV_END if (rc == -1) { syslog(LOG_WARNING, "could not lock job %lu: %m", jobno); free(mailname); free(newname); return; } /* If something goes wrong between here and the unlink() call, * the job gets restarted as soon as the "=" entry is cleared * by the main atd loop. */ pid = fork(); if (pid == -1) { lerr("Cannot fork for job execution"); free(mailname); free(newname); return; } else if (pid != 0) { free(mailname); free(newname); return; } (void) setsid(); /* own session for process */ /* Let's see who we mail to. Hopefully, we can read it from * the command file; if not, send it to the owner, or, failing that, * to root. */ pentry = getpwuid(uid); if (pentry == NULL) { pabort("Userid %lu not found - aborting job %8lu (%.500s)", (unsigned long) uid, jobno, filename); } syslog(LOG_INFO, "Starting job %lu (%.500s) for user '%s' (%lu)", jobno, filename, pentry->pw_name, (unsigned long) uid); PRIV_START stream = fopen(filename, "r"); PRIV_END if (stream == NULL) perr("Cannot open input file"); if ((fd_in = dup(fileno(stream))) < 0) perr("Error duplicating input file descriptor"); if (fstat(fd_in, &buf) == -1) perr("Error in fstat of input file descriptor"); if (lstat(filename, &lbuf) == -1) perr("Error in fstat of input file"); if (S_ISLNK(lbuf.st_mode)) perr("Symbolic link encountered in job %8lu (%.500s) - aborting", jobno, filename); if ((lbuf.st_dev != buf.st_dev) || (lbuf.st_ino != buf.st_ino) || (lbuf.st_uid != buf.st_uid) || (lbuf.st_gid != buf.st_gid) || (lbuf.st_size != buf.st_size)) perr("Somebody changed files from under us for job %8lu (%.500s) - " "aborting", jobno, filename); if (buf.st_nlink > 2) { perr("Somebody is trying to run a linked script for job %8lu (%.500s)", jobno, filename); } if ((fflags = fcntl(fd_in, F_GETFD)) < 0) perr("Error in fcntl"); fcntl(fd_in, F_SETFD, fflags & ~FD_CLOEXEC); if (flock(fd_in, LOCK_EX | LOCK_NB) != 0) perr("Somebody already locked the job %8lu (%.500s) - " "aborting", jobno, filename); /* * If the spool directory is mounted via NFS `atd' isn't able to * read from the job file and will bump out here. The file is * opened as "root" but it is read as "daemon" which fails over * NFS and works with local file systems. It's not clear where * the bug is located. -Joey */ sprintf(fmt, "#!/bin/sh\n# atrun uid=%%d gid=%%d\n# mail %%%ds %%d", mailsize ); /* Unlink the file unless there was an error reading it (perhaps * temporary). * If the file has a bogus format there is no reason in trying * to run it again and again. */ if (fscanf(stream, fmt, &nuid, &ngid, mailname, &send_mail) != 4) { if (ferror(stream)) perr("Error reading the job file"); unlink(filename); pabort("File %.500s is in wrong format - aborting", filename); } unlink(filename); if (mailname[0] == '-') pabort("illegal mail name %.300s in job %8lu (%.300s)", mailname, jobno, filename); if (nuid != uid) pabort("Job %8lu (%.500s) - userid %d does not match file uid %d", jobno, filename, nuid, uid); fclose(stream); if (chdir(ATSPOOL_DIR) < 0) perr("Cannot chdir to " ATSPOOL_DIR); /* Create a file to hold the output of the job we are about to run. * Write the mail header. Complain in case */ if (unlink(filename) != -1) { syslog(LOG_WARNING,"Warning: for duplicate output file for %.100s (dead job?)", filename); } if ((fd_out = open(filename, O_RDWR | O_CREAT | O_EXCL, S_IWUSR | S_IRUSR)) < 0) perr("Cannot create output file"); PRIV_START if (fchown(fd_out, uid, ngid) == -1) syslog(LOG_WARNING, "Warning: could not change owner of output file for job %li to %i:%i: %s", jobno, uid, ngid, strerror(errno)); PRIV_END write_string(fd_out, "Subject: Output from your job "); write_string(fd_out, jobbuf); if (mail_with_hostname > 0) { gethostname(hostbuf, MAXHOSTNAMELEN-1); write_string(fd_out, " "); write_string(fd_out, hostbuf); } write_string(fd_out, "\nTo: "); write_string(fd_out, mailname); write_string(fd_out, "\n\n"); fstat(fd_out, &buf); size = buf.st_size; #ifdef WITH_PAM AT_START_PAM; AT_OPEN_PAM_SESSION; closelog(); openlog("atd", LOG_PID, LOG_ATD); #endif close(STDIN_FILENO); close(STDOUT_FILENO); close(STDERR_FILENO); pid = fork(); if (pid < 0) perr("Error in fork"); else if (pid == 0) { char *nul = NULL; char **nenvp = &nul; char **pam_envp=0L; PRIV_START #ifdef WITH_PAM pam_envp = pam_getenvlist(pamh); if ( ( pam_envp != 0L ) && (pam_envp[0] != 0L) ) nenvp = pam_envp; #endif /* Set up things for the child; we want standard input from the * input file, and standard output and error sent to our output file. */ if (lseek(fd_in, (off_t) 0, SEEK_SET) < 0) perr("Error in lseek"); if (dup2(fd_in, STDIN_FILENO) < 0) perr("Error in I/O redirection"); if (dup2(fd_out, STDOUT_FILENO) < 0) perr("Error in I/O redirection"); if (dup2(fd_out, STDERR_FILENO) < 0) perr("Error in I/O redirection"); close(fd_in); close(fd_out); nice((tolower((int) queue) - 'a' + 1) * 2); #ifdef WITH_SELINUX if (selinux_enabled > 0) { if (set_selinux_context(pentry->pw_name, filename) < 0) perr("SELinux Failed to set context\n"); } #endif if (initgroups(pentry->pw_name, pentry->pw_gid)) perr("Cannot initialize the supplementary group access list"); if (setgid(ngid) < 0) perr("Cannot change group"); if (setuid(uid) < 0) perr("Cannot set user id"); if (SIG_ERR == signal(SIGCHLD, SIG_DFL)) perr("Cannot reset signal handler to default"); chdir("/"); execle("/bin/sh", "sh", (char *) NULL, nenvp); perr("Exec failed for /bin/sh"); /* perr exits, the PRIV_END is just for nice form */ PRIV_END } /* We're the parent. Let's wait. We inherited the master's SIGCHLD handler, which does a non-blocking waitpid. So this blocking one will eventually return with an ECHILD error. */ waitpid(pid, (int *) NULL, 0); /* Send mail. Unlink the output file after opening it, so it * doesn't hang around after the run. */ fstat(fd_out, &buf); lseek(fd_out, 0, SEEK_SET); if (dup2(fd_out, STDIN_FILENO) < 0) perr("Could not use jobfile as standard input."); /* some sendmail implementations are confused if stdout, stderr are * not available, so let them point to /dev/null */ if ((fd_std = open("/dev/null", O_WRONLY)) < 0) perr("Could not open /dev/null."); if (dup2(fd_std, STDOUT_FILENO) < 0) perr("Could not use /dev/null as standard output."); if (dup2(fd_std, STDERR_FILENO) < 0) perr("Could not use /dev/null as standard error."); if (fd_std != STDOUT_FILENO && fd_std != STDERR_FILENO) close(fd_std); if (unlink(filename) == -1) syslog(LOG_WARNING, "Warning: removing output file for job %li failed: %s", jobno, strerror(errno)); /* The job is now finished. We can delete its input file. */ if (chdir(ATJOB_DIR) != 0) perr("Somebody removed %s directory from under us.", ATJOB_DIR); /* This also removes the flock */ (void)close(fd_in); unlink(newname); free(newname); #ifdef ATD_MAIL_PROGRAM if (((send_mail != -1) && (buf.st_size != size)) || (send_mail == 1)) { int mail_pid = -1; mail_pid = fork(); if ( mail_pid == 0 ) { PRIV_START if (initgroups(pentry->pw_name, pentry->pw_gid)) perr("Cannot initialize the supplementary group access list"); if (setgid(gid) < 0) perr("Cannot change group"); if (setuid(uid) < 0) perr("Cannot set user id"); if (SIG_ERR == signal(SIGCHLD, SIG_DFL)) perr("Cannot reset signal handler to default"); chdir ("/"); #if defined(SENDMAIL) execl(SENDMAIL, "sendmail", "-i", mailname, (char *) NULL); #else #error "No mail command specified." #endif perr("Exec failed for mail command"); PRIV_END } else if ( mail_pid == -1 ) { syslog(LOG_ERR, "fork of mailer failed: %m"); } /* Parent */ waitpid(mail_pid, (int *) NULL, 0); } #ifdef WITH_PAM AT_CLOSE_PAM; closelog(); openlog("atd", LOG_PID, LOG_ATD); #endif #endif exit(EXIT_SUCCESS); } static time_t run_loop() { DIR *spool; struct dirent *dirent; struct stat buf; unsigned long ctm; unsigned long jobno; char queue; char batch_queue = '\0'; time_t run_time, next_job; char batch_name[] = "z2345678901234"; char lock_name[] = "z2345678901234"; uid_t batch_uid; gid_t batch_gid; int run_batch; static time_t next_batch = 0; double currlavg[3]; /* Main loop. Open spool directory for reading and look over all the * files in there. If the filename indicates that the job should be run, * run a function which sets its user and group id to that of the files * and execs a /bin/sh, which executes the shell. The function will * then remove the script (hopefully). * * Also, pick the oldest batch job to run, at most one per run of * the main loop. */ next_job = now + CHECK_INTERVAL; if (next_batch == 0) next_batch = now; /* To avoid spinning up the disk unnecessarily, stat the directory and * return immediately if it hasn't changed since the last time we woke * up. */ if (stat(".", &buf) == -1) { lerr("Cannot stat " ATJOB_DIR); return next_job; } if (nothing_to_do && buf.st_mtime <= last_chg) return next_job; last_chg = buf.st_mtime; if ((spool = opendir(".")) == NULL) { lerr("Cannot read " ATJOB_DIR); return next_job; } run_batch = 0; nothing_to_do = 1; batch_uid = (uid_t) - 1; batch_gid = (gid_t) - 1; while ((dirent = readdir(spool)) != NULL) { /* Avoid the stat if this doesn't look like a job file */ if (sscanf(dirent->d_name, "%c%5lx%8lx", &queue, &jobno, &ctm) != 3) continue; /* Chances are a '=' file has been deleted from under us. * Ignore. */ if (stat(dirent->d_name, &buf) != 0) continue; if (!S_ISREG(buf.st_mode)) continue; /* We don't want files which at(1) hasn't yet marked executable. */ if (!(buf.st_mode & S_IXUSR)) { nothing_to_do = 0; /* it will probably become executable soon */ continue; } run_time = (time_t) ctm *60; /* Skip lock files */ if (queue == '=') { if ((buf.st_nlink == 1) && (run_time + CHECK_INTERVAL <= now)) { int fd; fd = open(dirent->d_name, O_RDONLY); if (fd != -1) { if (flock(fd, LOCK_EX | LOCK_NB) == 0) { unlink(dirent->d_name); syslog(LOG_NOTICE, "removing stale lock file %s\n", dirent->d_name); } (void)close(fd); } } continue; } /* Skip any other file types which may have been invented in * the meantime. */ if (!(isupper(queue) || islower(queue))) { continue; } /* Is the file already locked? */ if (buf.st_nlink > 1) { if (run_time < buf.st_mtime) run_time = buf.st_mtime; if (run_time + CHECK_INTERVAL <= now) { /* Something went wrong the last time this was executed. * Let's remove the lockfile and reschedule. * We also change the timestamp to avoid rerunning the job more * than once every CHECK_INTERVAL. */ strncpy(lock_name, dirent->d_name, sizeof(lock_name)); if (utime(lock_name, 0) < 0) syslog(LOG_ERR, "utime couldn't be set for lock file %s\n", lock_name); lock_name[sizeof(lock_name)-1] = '\0'; lock_name[0] = '='; unlink(lock_name); next_job = now; nothing_to_do = 0; } continue; } /* If we got here, then there are jobs of some kind waiting. * We could try to be smarter and leave nothing_to_do set if * we end up processing all the jobs, but that's risky (run_file * might fail and expect the job to be rescheduled), and it doesn't * gain us much. */ nothing_to_do = 0; /* There's a job for later. Note its execution time if it's * the earliest so far. */ if (run_time > now) { if (next_job > run_time) { next_job = run_time; } continue; } if (isbatch(queue)) { /* We could potentially run this batch job. If it's scheduled * at a higher priority than anything before, keep its * filename. */ run_batch++; if (strcmp(batch_name, dirent->d_name) > 0) { strncpy(batch_name, dirent->d_name, sizeof(batch_name)); batch_name[sizeof(batch_name)-1] = '\0'; batch_uid = buf.st_uid; batch_gid = buf.st_gid; batch_queue = queue; } } else { if (run_time <= now) { run_file(dirent->d_name, buf.st_uid, buf.st_gid); } } } closedir(spool); /* run the single batch file, if any */ if (run_batch && (next_batch <= now)) { next_batch = now + batch_interval; #ifdef GETLOADAVG_PRIVILEGED START_PRIV #endif if (getloadavg(currlavg, 1) < 1) { currlavg[0] = 0.0; } #ifdef GETLOADAVG_PRIVILEGED END_PRIV #endif if (currlavg[0] < load_avg) { run_file(batch_name, batch_uid, batch_gid); run_batch--; } } if (run_batch && (next_batch < next_job)) { nothing_to_do = 0; next_job = next_batch; } return next_job; } #ifdef HAVE_TIMER_CREATE timer_t timer; struct itimerspec timeout; void timer_setup() { struct sigevent sev; sev.sigev_notify = SIGEV_SIGNAL; sev.sigev_signo = SIGHUP; sev.sigev_value.sival_ptr = &timer; memset(&timeout, 0, sizeof(timeout)); if (timer_create(CLOCK_REALTIME, &sev, &timer) < 0) pabort("unable to create timer"); } time_t atd_gettime() { struct timespec curtime; clock_gettime(CLOCK_REALTIME, &curtime); return curtime.tv_sec; } void atd_setalarm(time_t next) { timeout.it_value.tv_sec = next; timer_settime(timer, TIMER_ABSTIME, &timeout, NULL); pause(); } #else void timer_setup() { } time_t atd_gettime() { return time(NULL); } void atd_setalarm(time_t next) { sleep(next - atd_gettime()); } #endif /* Global functions */ int main(int argc, char *argv[]) { /* Browse through ATJOB_DIR, checking all the jobfiles whether they should * be executed and or deleted. The queue is coded into the first byte of * the job filename, the next 5 bytes encode the serial number in hex, and * the final 8 bytes encode the date (minutes since Eon) in hex. A file * which has not been executed yet is denoted by its execute - bit set. * For those files which are to be executed, run_file() is called, which forks * off a child which takes care of I/O redirection, forks off another child * for execution and yet another one, optionally, for sending mail. * Files which already have run are removed during the next invocation. */ int c; time_t next_invocation; struct sigaction act; struct passwd *pwe; struct group *ge; #ifdef WITH_SELINUX selinux_enabled=is_selinux_enabled(); if (selinux_enabled) { selinux_set_callback(SELINUX_CB_LOG, (union selinux_callback) selinux_log_callback); } #endif /* We don't need root privileges all the time; running under uid and gid * daemon is fine. */ if ((pwe = getpwnam(DAEMON_USERNAME)) == NULL) perr("Cannot get uid for " DAEMON_USERNAME); daemon_uid = pwe->pw_uid; if ((ge = getgrnam(DAEMON_GROUPNAME)) == NULL) perr("Cannot get gid for " DAEMON_GROUPNAME); daemon_gid = ge->gr_gid; RELINQUISH_PRIVS_ROOT(daemon_uid, daemon_gid) #ifndef LOG_CRON #define LOG_CRON LOG_DAEMON #endif openlog("atd", LOG_PID, LOG_CRON); opterr = 0; errno = 0; run_as_daemon = 1; batch_interval = BATCH_INTERVAL_DEFAULT; while ((c = getopt(argc, argv, "sdnl:b:f")) != EOF) { switch (c) { case 'l': if (sscanf(optarg, "%lf", &load_avg) != 1) pabort("garbled option -l"); if (load_avg <= 0.) load_avg = LOADAVG_MX; break; case 'b': if (sscanf(optarg, "%ud", &batch_interval) != 1) pabort("garbled option -b"); break; case 'd': daemon_debug++; daemon_foreground++; break; case 'f': daemon_foreground++; break; case 'n': mail_with_hostname=1; break; case 's': run_as_daemon = 0; break; case '?': pabort("unknown option"); break; default: pabort("idiotic option - aborted"); break; } } namep = argv[0]; if (chdir(ATJOB_DIR) != 0) perr("Cannot change to " ATJOB_DIR); if (optind < argc) pabort("non-option arguments - not allowed"); sigaction(SIGCHLD, NULL, &act); act.sa_handler = release_zombie; act.sa_flags = SA_NOCLDSTOP; sigaction(SIGCHLD, &act, NULL); if (!run_as_daemon) { now = atd_gettime(); run_loop(); exit(EXIT_SUCCESS); } /* Main loop. Let's sleep for a specified interval, * or until the next job is scheduled, or until we get signaled. * After any of these events, we rescan the queue. * A signal handler setting term_signal will make sure there's * a clean exit. */ sigaction(SIGHUP, NULL, &act); act.sa_handler = sdummy; sigaction(SIGHUP, &act, NULL); sigaction(SIGTERM, NULL, &act); act.sa_handler = set_term; sigaction(SIGTERM, &act, NULL); sigaction(SIGINT, NULL, &act); act.sa_handler = set_term; sigaction(SIGINT, &act, NULL); timer_setup(); daemon_setup(); do { now = atd_gettime(); next_invocation = run_loop(); if (next_invocation > now) { atd_setalarm(next_invocation); } } while (!term_signal); daemon_cleanup(); exit(EXIT_SUCCESS); }