2 * FCRON - periodic command scheduler
4 * Copyright 2000-2014 Thibault Godouet <fcron@free.fr>
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20 * The GNU General Public License can also be found in the file
21 * `LICENSE' that comes with the fcron source distribution.
24 /* code to handle system suspend/hibernate and resume */
28 #include "fcronconf.h"
31 #ifdef HAVE_SYS_TIMERFD_H
32 #include <sys/select.h>
33 #include <sys/timerfd.h>
34 #include <stdint.h> /* for uint64_t */
36 /* Not too sure how to get that from the Linux header, so we redefine here instead */
37 #ifndef TFD_TIMER_CANCEL_ON_SET
38 #define TFD_TIMER_CANCEL_ON_SET (1 << 1)
39 #endif /* TFD_TIMER_CANCEL_ON_SET */
40 #endif /* __linux__ */
41 #endif /* HAVE_SYS_TIMERFD_H */
44 #if defined(CLOCK_BOOTTIME) && defined(CLOCK_MONOTONIC)
45 struct linux_timeref {
46 struct timespec monotonic;
47 struct timespec boottime;
49 #endif /* defined(CLOCK_BOOTTIME) && defined(CLOCK_MONOTONIC) */
52 #if defined(CLOCK_BOOTTIME) && defined(CLOCK_MONOTONIC)
53 struct linux_timeref linux_suspend_ref;
54 #endif /* defined(CLOCK_BOOTTIME) && defined(CLOCK_MONOTONIC) */
55 #ifdef TFD_TIMER_CANCEL_ON_SET
56 int timer_cancel_on_set_fd = -1;
57 #endif /* TFD_TIMER_CANCEL_ON_SET */
60 /* internal functions */
61 long int read_suspend_duration(time_t slept_from);
62 double timespec_diff(struct timespec *from, struct timespec *to);
63 #if defined(CLOCK_BOOTTIME) && defined(CLOCK_MONOTONIC)
64 void linux_init_timeref(struct linux_timeref *tr);
65 double linux_get_suspend_time(struct linux_timeref *tr);
66 #endif /* defined(CLOCK_BOOTTIME) && defined(CLOCK_MONOTONIC) */
69 timespec_diff(struct timespec *from, struct timespec *to)
71 return ((to->tv_sec - from->tv_sec) * 1000000000.0 +
72 (to->tv_nsec - from->tv_nsec)) / 1000000000.0;
75 #if defined(CLOCK_BOOTTIME) && defined(CLOCK_MONOTONIC)
77 linux_init_timeref(struct linux_timeref *tr)
78 /* Initialize the time reference */
80 clock_gettime(CLOCK_MONOTONIC, &tr->monotonic);
81 clock_gettime(CLOCK_BOOTTIME, &tr->boottime);
85 linux_get_suspend_time(struct linux_timeref *tr)
86 /* return the suspend time, using Linux-specific APIs */
88 struct timespec tpm, tpb;
89 double tpm_diff, tpb_diff;
91 debug("Calculating suspend duration from CLOCK_BOOTTIME-CLOCK_MONOTONIC");
93 clock_gettime(CLOCK_MONOTONIC, &tpm);
94 clock_gettime(CLOCK_BOOTTIME, &tpb);
96 tpm_diff = timespec_diff(&tr->monotonic, &tpm);
97 tpb_diff = timespec_diff(&tr->boottime, &tpb);
99 linux_init_timeref(tr);
101 return (tpb_diff - tpm_diff);
103 #endif /* defined(CLOCK_BOOTTIME) && defined(CLOCK_MONOTONIC) */
106 read_suspend_duration(time_t slept_from)
107 /* Return the amount of time the system was suspended (to mem or disk),
108 * as read from suspendfile.
112 * 1) the OS sends the STOP signal to the main fcron process when suspending
113 * 2) the OS writes the suspend duration (as a string) into suspendfile,
114 * and then sends the CONT signal to the main fcron process when resuming.
116 * The main reason to do it this way instead of killing fcron and restarting
117 * it on resume is to better handle jobs that may already be running.
118 * (e.g. don't run them again when the machine resumes) */
123 long int suspend_duration = 0; /* default value to return on error */
126 debug("Attempting to read suspend duration from %s", suspendfile);
127 fd = open(suspendfile, O_RDONLY | O_NONBLOCK);
129 /* If the file doesn't exist, then we assume the user/system
130 * did a manual 'kill -STOP' / 'kill -CONT' and doesn't intend
131 * for fcron to account for any suspend time.
132 * This is not considered as an error. */
133 if (errno != ENOENT) {
134 error_e("Could not open suspend file '%s'", suspendfile);
139 /* check the file is a 'normal' file (e.g. not a link) and only writable
140 * by root -- don't allow attacker to affect job schedules,
141 * or delete the suspendfile */
142 if (fstat(fd, &s) < 0) {
143 error_e("could not fstat() suspend file '%s'", suspendfile);
146 if (!S_ISREG(s.st_mode) || s.st_nlink != 1) {
147 error_e("suspend file %s is not a regular file", suspendfile);
151 if (s.st_mode & S_IWOTH || s.st_uid != rootuid || s.st_gid != rootgid) {
152 error("suspend file %s must be owned by %s:%s and not writable by"
153 " others.", suspendfile, ROOTNAME, ROOTGROUP);
157 /* read the content of the suspendfile into the buffer */
158 read_len = read(fd, buf, sizeof(buf) - 1);
160 /* we have to run this immediately or errno may be changed */
161 error_e("Could not read suspend file '%s'", suspendfile);
162 goto unlink_cleanup_return;
165 goto unlink_cleanup_return;
167 buf[read_len] = '\0';
170 suspend_duration = strtol(buf, NULL, 10);
172 error_e("Count not parse suspend duration '%s'", buf);
173 suspend_duration = 0;
174 goto unlink_cleanup_return;
176 else if (suspend_duration < 0) {
177 warn("Read negative suspend_duration (%ld): ignoring.");
178 suspend_duration = 0;
179 goto unlink_cleanup_return;
182 debug("Read suspend_duration of '%ld' from suspend file '%s'",
183 suspend_duration, suspendfile);
185 if (now < slept_from + suspend_duration) {
186 long int time_slept = now - slept_from;
188 /* we can have a couple of seconds more due to rounding up,
189 * but anything more should be an invalid value in suspendfile */
190 explain("Suspend duration %lds in suspend file '%s' is longer than "
191 "we slept. This could be due to rounding. "
192 "Reverting to time slept %lds.",
193 suspend_duration, suspendfile, time_slept);
194 suspend_duration = time_slept;
198 unlink_cleanup_return:
199 if (unlink(suspendfile) < 0) {
200 warn_e("Could not remove suspend file '%s'", suspendfile);
205 if (fd >= 0 && xclose(&fd) < 0) {
206 warn_e("Could not xclose() suspend file '%s'", suspendfile);
209 return suspend_duration;
214 init_suspend(select_instance * si)
217 #if defined(CLOCK_BOOTTIME) && defined(CLOCK_MONOTONIC)
218 linux_init_timeref(&linux_suspend_ref);
219 #endif /* defined(CLOCK_BOOTTIME) && defined(CLOCK_MONOTONIC) */
221 #ifdef TFD_TIMER_CANCEL_ON_SET
223 struct itimerspec expiration = {
224 .it_value.tv_sec = TIME_T_MAX,
228 timer_cancel_on_set_fd = timerfd_create(CLOCK_REALTIME, 0);
229 if (timer_cancel_on_set_fd == -1) {
230 die_e("timerfd_create");
233 (timer_cancel_on_set_fd,
234 TFD_TIMER_ABSTIME | TFD_TIMER_CANCEL_ON_SET, &expiration,
236 die_e("timerfd_settime");
240 select_add_read(si, timer_cancel_on_set_fd);
241 #endif /* TFD_TIMER_CANCEL_ON_SET */
247 check_suspend(time_t slept_from, time_t nwt, char *sig_cont,
248 select_instance * si)
249 /* Check if the machine was suspended (to mem or disk), and if so
250 * reschedule jobs accordingly */
252 long int suspend_duration = 0; /* amount of time the system was suspended */
253 long int time_diff; /* estimate of suspend_duration (as fallback) */
255 time_diff = now - nwt;
257 #ifdef TFD_TIMER_CANCEL_ON_SET
258 if (si->retcode > 0 && FD_ISSET(timer_cancel_on_set_fd, &si->readfds)) {
259 /* we don't need the data from that fd, but we must read it
260 * or select() would return immediately again */
262 read(timer_cancel_on_set_fd, &exp, sizeof(uint64_t));
265 ("We were notified of a change of time, find out suspend duration");
266 #ifdef JUST_FOR_FORMATTING
270 #else /* TFD_TIMER_CANCEL_ON_SET */
273 debug("We received a SIGCONT, find out the suspend duration");
275 #endif /* TFD_TIMER_CANCEL_ON_SET */
277 #if defined(CLOCK_BOOTTIME) && defined(CLOCK_MONOTONIC)
278 suspend_duration = linux_get_suspend_time(&linux_suspend_ref);
279 #else /* defined(CLOCK_BOOTTIME) && defined(CLOCK_MONOTONIC) */
280 suspend_duration = read_suspend_duration(slept_from);
281 #endif /* defined(CLOCK_BOOTTIME) && defined(CLOCK_MONOTONIC) */
286 /* Also check if there was an unaccounted sleep duration, in case
287 * the OS is not configured to let fcron properly know about suspends
289 * This is not perfect as we may miss some suspend time if fcron
290 * is woken up before the timer expiry, e.g. due to a signal
291 * or activity on a socket (fcrondyn).
292 * NOTE: the +5 second is arbitrary -- just a way to make sure
293 * we don't get any false positive. If the suspend or hibernate
294 * is very short it seems fine to simply ignore it anyway */
295 if (suspend_duration <= 0 && time_diff > 5) {
296 #if defined(CLOCK_BOOTTIME) && defined(CLOCK_MONOTONIC)
297 suspend_duration = linux_get_suspend_time(&linux_suspend_ref);
298 #else /* defined(CLOCK_BOOTTIME) && defined(CLOCK_MONOTONIC) */
299 suspend_duration = time_diff;
300 #endif /* defined(CLOCK_BOOTTIME) && defined(CLOCK_MONOTONIC) */
303 if (suspend_duration > 0) {
304 long int actual_sleep = now - slept_from;
305 long int scheduled_sleep = nwt - slept_from;
307 explain("suspend/hibernate detected: we woke up after %lus"
308 " instead of %lus. The system was suspended for %lus.",
309 actual_sleep, scheduled_sleep, suspend_duration);
310 reschedule_all_on_resume(suspend_duration);