/* * FCRON - periodic command scheduler * * Copyright 2000-2014 Thibault Godouet * * 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. */ /* code to handle system suspend/hibernate and resume */ #include "suspend.h" #include "global.h" #include "fcronconf.h" #include "fcron.h" #include "database.h" #ifdef HAVE_SYS_TIMERFD_H #include #include #include /* for uint64_t */ #ifdef __linux__ /* Not too sure how to get that from the Linux header, so we redefine here instead */ #ifndef TFD_TIMER_CANCEL_ON_SET #define TFD_TIMER_CANCEL_ON_SET (1 << 1) #endif /* TFD_TIMER_CANCEL_ON_SET */ #endif /* __linux__ */ #endif /* HAVE_SYS_TIMERFD_H */ /* types */ #if defined(CLOCK_BOOTTIME) && defined(CLOCK_MONOTONIC) struct linux_timeref { struct timespec monotonic; struct timespec boottime; }; #endif /* defined(CLOCK_BOOTTIME) && defined(CLOCK_MONOTONIC) */ /* variables */ #if defined(CLOCK_BOOTTIME) && defined(CLOCK_MONOTONIC) struct linux_timeref linux_suspend_ref; #endif /* defined(CLOCK_BOOTTIME) && defined(CLOCK_MONOTONIC) */ #ifdef TFD_TIMER_CANCEL_ON_SET int timer_cancel_on_set_fd = -1; #endif /* TFD_TIMER_CANCEL_ON_SET */ /* internal functions */ long int read_suspend_duration(time_t slept_from); double timespec_diff(struct timespec *from, struct timespec *to); #if defined(CLOCK_BOOTTIME) && defined(CLOCK_MONOTONIC) void linux_init_timeref(struct linux_timeref *tr); double linux_get_suspend_time(struct linux_timeref *tr); #endif /* defined(CLOCK_BOOTTIME) && defined(CLOCK_MONOTONIC) */ double timespec_diff(struct timespec *from, struct timespec *to) { return ((to->tv_sec - from->tv_sec) * 1000000000.0 + (to->tv_nsec - from->tv_nsec)) / 1000000000.0; } #if defined(CLOCK_BOOTTIME) && defined(CLOCK_MONOTONIC) void linux_init_timeref(struct linux_timeref *tr) /* Initialize the time reference */ { clock_gettime(CLOCK_MONOTONIC, &tr->monotonic); clock_gettime(CLOCK_BOOTTIME, &tr->boottime); } double linux_get_suspend_time(struct linux_timeref *tr) /* return the suspend time, using Linux-specific APIs */ { struct timespec tpm, tpb; double tpm_diff, tpb_diff; debug("Calculating suspend duration from CLOCK_BOOTTIME-CLOCK_MONOTONIC"); clock_gettime(CLOCK_MONOTONIC, &tpm); clock_gettime(CLOCK_BOOTTIME, &tpb); tpm_diff = timespec_diff(&tr->monotonic, &tpm); tpb_diff = timespec_diff(&tr->boottime, &tpb); linux_init_timeref(tr); return (tpb_diff - tpm_diff); } #endif /* defined(CLOCK_BOOTTIME) && defined(CLOCK_MONOTONIC) */ long int read_suspend_duration(time_t slept_from) /* Return the amount of time the system was suspended (to mem or disk), * as read from suspendfile. * Return 0 on error. * * The idea is that: * 1) the OS sends the STOP signal to the main fcron process when suspending * 2) the OS writes the suspend duration (as a string) into suspendfile, * and then sends the CONT signal to the main fcron process when resuming. * * The main reason to do it this way instead of killing fcron and restarting * it on resume is to better handle jobs that may already be running. * (e.g. don't run them again when the machine resumes) */ { int fd = -1; char buf[TERM_LEN]; int read_len = 0; long int suspend_duration = 0; /* default value to return on error */ struct stat s; debug("Attempting to read suspend duration from %s", suspendfile); fd = open(suspendfile, O_RDONLY | O_NONBLOCK); if (fd == -1) { /* If the file doesn't exist, then we assume the user/system * did a manual 'kill -STOP' / 'kill -CONT' and doesn't intend * for fcron to account for any suspend time. * This is not considered as an error. */ if (errno != ENOENT) { error_e("Could not open suspend file '%s'", suspendfile); } goto cleanup_return; } /* check the file is a 'normal' file (e.g. not a link) and only writable * by root -- don't allow attacker to affect job schedules, * or delete the suspendfile */ if (fstat(fd, &s) < 0) { error_e("could not fstat() suspend file '%s'", suspendfile); goto cleanup_return; } if (!S_ISREG(s.st_mode) || s.st_nlink != 1) { error_e("suspend file %s is not a regular file", suspendfile); goto cleanup_return; } if (s.st_mode & S_IWOTH || s.st_uid != rootuid || s.st_gid != rootgid) { error("suspend file %s must be owned by %s:%s and not writable by" " others.", suspendfile, ROOTNAME, ROOTGROUP); goto cleanup_return; } /* read the content of the suspendfile into the buffer */ read_len = read(fd, buf, sizeof(buf) - 1); if (read_len < 0) { /* we have to run this immediately or errno may be changed */ error_e("Could not read suspend file '%s'", suspendfile); goto unlink_cleanup_return; } if (read_len < 0) { goto unlink_cleanup_return; } buf[read_len] = '\0'; errno = 0; suspend_duration = strtol(buf, NULL, 10); if (errno != 0) { error_e("Count not parse suspend duration '%s'", buf); suspend_duration = 0; goto unlink_cleanup_return; } else if (suspend_duration < 0) { warn("Read negative suspend_duration (%ld): ignoring."); suspend_duration = 0; goto unlink_cleanup_return; } else { debug("Read suspend_duration of '%ld' from suspend file '%s'", suspend_duration, suspendfile); if (now < slept_from + suspend_duration) { long int time_slept = now - slept_from; /* we can have a couple of seconds more due to rounding up, * but anything more should be an invalid value in suspendfile */ explain("Suspend duration %lds in suspend file '%s' is longer than " "we slept. This could be due to rounding. " "Reverting to time slept %lds.", suspend_duration, suspendfile, time_slept); suspend_duration = time_slept; } } unlink_cleanup_return: if (unlink(suspendfile) < 0) { warn_e("Could not remove suspend file '%s'", suspendfile); return 0; } cleanup_return: if (fd >= 0 && xclose(&fd) < 0) { warn_e("Could not xclose() suspend file '%s'", suspendfile); } return suspend_duration; } void init_suspend(select_instance * si) { #if defined(CLOCK_BOOTTIME) && defined(CLOCK_MONOTONIC) linux_init_timeref(&linux_suspend_ref); #endif /* defined(CLOCK_BOOTTIME) && defined(CLOCK_MONOTONIC) */ #ifdef TFD_TIMER_CANCEL_ON_SET { struct itimerspec expiration = { .it_value.tv_sec = TIME_T_MAX, }; timer_cancel_on_set_fd = timerfd_create(CLOCK_REALTIME, 0); if (timer_cancel_on_set_fd == -1) { die_e("timerfd_create"); } if (timerfd_settime (timer_cancel_on_set_fd, TFD_TIMER_ABSTIME | TFD_TIMER_CANCEL_ON_SET, &expiration, NULL) == -1) { die_e("timerfd_settime"); } } select_add_read(si, timer_cancel_on_set_fd); #endif /* TFD_TIMER_CANCEL_ON_SET */ } void check_suspend(time_t slept_from, time_t nwt, char *sig_cont, select_instance * si) /* Check if the machine was suspended (to mem or disk), and if so * reschedule jobs accordingly */ { long int suspend_duration = 0; /* amount of time the system was suspended */ long int time_diff; /* estimate of suspend_duration (as fallback) */ time_diff = now - nwt; #ifdef TFD_TIMER_CANCEL_ON_SET if (si->retcode > 0 && FD_ISSET(timer_cancel_on_set_fd, &si->readfds)) { /* we don't need the data from that fd, but we must read it * or select() would return immediately again */ uint64_t exp; read(timer_cancel_on_set_fd, &exp, sizeof(uint64_t)); debug ("We were notified of a change of time, find out suspend duration"); #ifdef JUST_FOR_FORMATTING } #endif #else /* TFD_TIMER_CANCEL_ON_SET */ if (*sig_cont > 0) { debug("We received a SIGCONT, find out the suspend duration"); #endif /* TFD_TIMER_CANCEL_ON_SET */ #if defined(CLOCK_BOOTTIME) && defined(CLOCK_MONOTONIC) suspend_duration = linux_get_suspend_time(&linux_suspend_ref); #else /* defined(CLOCK_BOOTTIME) && defined(CLOCK_MONOTONIC) */ suspend_duration = read_suspend_duration(slept_from); #endif /* defined(CLOCK_BOOTTIME) && defined(CLOCK_MONOTONIC) */ } /* Also check if there was an unaccounted sleep duration, in case * the OS is not configured to let fcron properly know about suspends * via suspendfile. * This is not perfect as we may miss some suspend time if fcron * is woken up before the timer expiry, e.g. due to a signal * or activity on a socket (fcrondyn). * NOTE: the +5 second is arbitrary -- just a way to make sure * we don't get any false positive. If the suspend or hibernate * is very short it seems fine to simply ignore it anyway */ if (suspend_duration <= 0 && time_diff > 5) { #if defined(CLOCK_BOOTTIME) && defined(CLOCK_MONOTONIC) suspend_duration = linux_get_suspend_time(&linux_suspend_ref); #else /* defined(CLOCK_BOOTTIME) && defined(CLOCK_MONOTONIC) */ suspend_duration = time_diff; #endif /* defined(CLOCK_BOOTTIME) && defined(CLOCK_MONOTONIC) */ } if (suspend_duration > 0) { long int actual_sleep = now - slept_from; long int scheduled_sleep = nwt - slept_from; explain("suspend/hibernate detected: we woke up after %lus" " instead of %lus. The system was suspended for %lus.", actual_sleep, scheduled_sleep, suspend_duration); reschedule_all_on_resume(suspend_duration); } }