]> granicus.if.org Git - fcron/blob - suspend.c
added runatresume / @resume
[fcron] / suspend.c
1 /*
2  * FCRON - periodic command scheduler
3  *
4  *  Copyright 2000-2014 Thibault Godouet <fcron@free.fr>
5  *
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.
10  *
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.
15  *
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
19  *
20  *  The GNU General Public License can also be found in the file
21  *  `LICENSE' that comes with the fcron source distribution.
22  */
23
24 /* code to handle system suspend/hibernate and resume */
25
26 #include "suspend.h"
27 #include "global.h"
28 #include "fcronconf.h"
29 #include "fcron.h"
30 #include "database.h"
31 #ifdef HAVE_SYS_TIMERFD_H
32 #include <sys/select.h>
33 #include <sys/timerfd.h>
34 #include <stdint.h>             /* for uint64_t */
35 #ifdef __linux__
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 */
42
43 /* types */
44 #if defined(CLOCK_BOOTTIME) && defined(CLOCK_MONOTONIC)
45 struct linux_timeref {
46     struct timespec monotonic;
47     struct timespec boottime;
48 };
49 #endif                          /* defined(CLOCK_BOOTTIME) && defined(CLOCK_MONOTONIC) */
50
51 /* variables */
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 */
58
59
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) */
67
68 double
69 timespec_diff(struct timespec *from, struct timespec *to)
70 {
71     return ((to->tv_sec - from->tv_sec) * 1000000000.0 +
72             (to->tv_nsec - from->tv_nsec)) / 1000000000.0;
73 }
74
75 #if defined(CLOCK_BOOTTIME) && defined(CLOCK_MONOTONIC)
76 void
77 linux_init_timeref(struct linux_timeref *tr)
78     /* Initialize the time reference */
79 {
80     clock_gettime(CLOCK_MONOTONIC, &tr->monotonic);
81     clock_gettime(CLOCK_BOOTTIME, &tr->boottime);
82 }
83
84 double
85 linux_get_suspend_time(struct linux_timeref *tr)
86     /* return the suspend time, using Linux-specific APIs */
87 {
88     struct timespec tpm, tpb;
89     double tpm_diff, tpb_diff;
90
91     debug("Calculating suspend duration from CLOCK_BOOTTIME-CLOCK_MONOTONIC");
92
93     clock_gettime(CLOCK_MONOTONIC, &tpm);
94     clock_gettime(CLOCK_BOOTTIME, &tpb);
95
96     tpm_diff = timespec_diff(&tr->monotonic, &tpm);
97     tpb_diff = timespec_diff(&tr->boottime, &tpb);
98
99     linux_init_timeref(tr);
100
101     return (tpb_diff - tpm_diff);
102 }
103 #endif                          /* defined(CLOCK_BOOTTIME) && defined(CLOCK_MONOTONIC) */
104
105 long int
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.
109    * Return 0 on error.
110    *
111    * The idea is that:
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.
115    *
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) */
119 {
120     int fd = -1;
121     char buf[TERM_LEN];
122     int read_len = 0;
123     long int suspend_duration = 0;      /* default value to return on error */
124     struct stat s;
125
126     debug("Attempting to read suspend duration from %s", suspendfile);
127     fd = open(suspendfile, O_RDONLY | O_NONBLOCK);
128     if (fd == -1) {
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);
135         }
136         goto cleanup_return;
137     }
138
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);
144         goto cleanup_return;
145     }
146     if (!S_ISREG(s.st_mode) || s.st_nlink != 1) {
147         error_e("suspend file %s is not a regular file", suspendfile);
148         goto cleanup_return;
149     }
150
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);
154         goto cleanup_return;
155     }
156
157     /* read the content of the suspendfile into the buffer */
158     read_len = read(fd, buf, sizeof(buf) - 1);
159     if (read_len < 0) {
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;
163     }
164     if (read_len < 0) {
165         goto unlink_cleanup_return;
166     }
167     buf[read_len] = '\0';
168
169     errno = 0;
170     suspend_duration = strtol(buf, NULL, 10);
171     if (errno != 0) {
172         error_e("Count not parse suspend duration '%s'", buf);
173         suspend_duration = 0;
174         goto unlink_cleanup_return;
175     }
176     else if (suspend_duration < 0) {
177         warn("Read negative suspend_duration (%ld): ignoring.");
178         suspend_duration = 0;
179         goto unlink_cleanup_return;
180     }
181     else {
182         debug("Read suspend_duration of '%ld' from suspend file '%s'",
183               suspend_duration, suspendfile);
184
185         if (now < slept_from + suspend_duration) {
186             long int time_slept = now - slept_from;
187
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;
195         }
196     }
197
198  unlink_cleanup_return:
199     if (unlink(suspendfile) < 0) {
200         warn_e("Could not remove suspend file '%s'", suspendfile);
201         return 0;
202     }
203
204  cleanup_return:
205     if (fd >= 0 && xclose(&fd) < 0) {
206         warn_e("Could not xclose() suspend file '%s'", suspendfile);
207     }
208
209     return suspend_duration;
210
211 }
212
213 void
214 init_suspend(select_instance * si)
215 {
216
217 #if defined(CLOCK_BOOTTIME) && defined(CLOCK_MONOTONIC)
218     linux_init_timeref(&linux_suspend_ref);
219 #endif                          /* defined(CLOCK_BOOTTIME) && defined(CLOCK_MONOTONIC) */
220
221 #ifdef TFD_TIMER_CANCEL_ON_SET
222     {
223         struct itimerspec expiration = {
224             .it_value.tv_sec = TIME_T_MAX,
225         };
226
227
228         timer_cancel_on_set_fd = timerfd_create(CLOCK_REALTIME, 0);
229         if (timer_cancel_on_set_fd == -1) {
230             die_e("timerfd_create");
231         }
232         if (timerfd_settime
233             (timer_cancel_on_set_fd,
234              TFD_TIMER_ABSTIME | TFD_TIMER_CANCEL_ON_SET, &expiration,
235              NULL) == -1) {
236             die_e("timerfd_settime");
237         }
238
239     }
240     select_add_read(si, timer_cancel_on_set_fd);
241 #endif                          /* TFD_TIMER_CANCEL_ON_SET */
242
243 }
244
245
246 void
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 */
251 {
252     long int suspend_duration = 0;      /* amount of time the system was suspended */
253     long int time_diff;         /* estimate of suspend_duration (as fallback) */
254
255     time_diff = now - nwt;
256
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 */
261         uint64_t exp;
262         read(timer_cancel_on_set_fd, &exp, sizeof(uint64_t));
263
264         debug
265             ("We were notified of a change of time, find out suspend duration");
266 #ifdef JUST_FOR_FORMATTING
267     }
268 #endif
269
270 #else                           /* TFD_TIMER_CANCEL_ON_SET */
271
272     if (*sig_cont > 0) {
273         debug("We received a SIGCONT, find out the suspend duration");
274
275 #endif                          /* TFD_TIMER_CANCEL_ON_SET */
276
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) */
282     }
283
284
285
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
288      * via suspendfile.
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) */
301     }
302
303     if (suspend_duration > 0) {
304         long int actual_sleep = now - slept_from;
305         long int scheduled_sleep = nwt - slept_from;
306
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);
311     }
312 }