| /*****************************************************************************\ |
| * cron.c |
| ***************************************************************************** |
| * Copyright (C) SchedMD LLC. |
| * |
| * This file is part of Slurm, a resource management program. |
| * For details, see <https://slurm.schedmd.com/>. |
| * Please also read the included file: DISCLAIMER. |
| * |
| * Slurm 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. |
| * |
| * In addition, as a special exception, the copyright holders give permission |
| * to link the code of portions of this program with the OpenSSL library under |
| * certain conditions as described in each individual source file, and |
| * distribute linked combinations including the two. You must obey the GNU |
| * General Public License in all respects for all of the code used other than |
| * OpenSSL. If you modify file(s) with this exception, you may extend this |
| * exception to your version of the file(s), but you are not obligated to do |
| * so. If you do not wish to do so, delete this exception statement from your |
| * version. If you delete this exception statement from all source files in |
| * the program, then also delete it here. |
| * |
| * Slurm 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 Slurm; if not, write to the Free Software Foundation, Inc., |
| * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
| \*****************************************************************************/ |
| |
| #include <ctype.h> |
| #include <unistd.h> |
| |
| #include "src/common/bitstring.h" |
| #include "src/common/cron.h" |
| #include "src/common/log.h" |
| #include "src/common/pack.h" |
| #include "src/common/read_config.h" |
| #include "src/common/slurm_time.h" |
| #include "src/common/uid.h" |
| #include "src/common/xmalloc.h" |
| #include "src/common/xstring.h" |
| |
| #define LEAP_YEAR(y) ((y % 4) == 0 && ((y % 100) != 0 || (y % 400) == 0)) |
| |
| extern cron_entry_t *new_cron_entry(void) |
| { |
| cron_entry_t *entry = xmalloc(sizeof(*entry)); |
| |
| entry->minute = bit_alloc(61); |
| entry->hour = bit_alloc(25); |
| entry->day_of_month = bit_alloc(32); |
| entry->month = bit_alloc(13); |
| entry->day_of_week = bit_alloc(8); |
| |
| return entry; |
| } |
| |
| extern void free_cron_entry(void *in) |
| { |
| cron_entry_t *entry = (cron_entry_t *) in; |
| |
| if (!entry) |
| return; |
| |
| xfree(entry->minute); |
| xfree(entry->hour); |
| xfree(entry->day_of_month); |
| xfree(entry->month); |
| xfree(entry->day_of_week); |
| xfree(entry->cronspec); |
| xfree(entry->command); |
| xfree(entry); |
| } |
| |
| extern bool valid_cron_entry(cron_entry_t *e) |
| { |
| int first_day_of_month; |
| |
| /* basic structure check */ |
| if ((bit_size(e->minute) != 61) || |
| (bit_size(e->hour) != 25) || |
| (bit_size(e->day_of_month) != 32) || |
| (bit_size(e->month) != 13) || |
| (bit_size(e->day_of_week) != 8)) |
| return false; |
| |
| /* |
| * Clear top or lower bits (may have been set for wildcard processing). |
| */ |
| bit_clear(e->minute, 60); |
| bit_clear(e->hour, 24); |
| bit_clear(e->day_of_month, 0); |
| bit_clear(e->month, 0); |
| bit_clear(e->day_of_week, 7); |
| |
| /* |
| * Missing some e. Need at least one bit set in each field or |
| * the wildcard flag, otherwise calc_next_cron_start() will break. |
| */ |
| first_day_of_month = bit_ffs(e->day_of_month); |
| if ((!(e->flags & CRON_WILD_MINUTE) && (bit_ffs(e->minute) == -1)) || |
| (!(e->flags & CRON_WILD_HOUR) && (bit_ffs(e->hour) == -1)) || |
| (!(e->flags & CRON_WILD_DOM) && (first_day_of_month == -1)) || |
| (!(e->flags & CRON_WILD_MONTH) && (bit_ffs(e->month) == -1)) || |
| (!(e->flags & CRON_WILD_DOW) && (bit_ffs(e->day_of_week) == -1))) |
| return false; |
| |
| /* |
| * Make sure the crontab isn't requesting a non-existent |
| * combination of month and day. |
| * |
| * Note: we do allow you to schedule something to only run |
| * on leap days, as crazy as that may seem. |
| */ |
| if (e->flags & CRON_WILD_DOM) { |
| ; |
| } else if (first_day_of_month == 31) { |
| if (!bit_test(e->month, 1) && !bit_test(e->month, 3) && |
| !bit_test(e->month, 5) && !bit_test(e->month, 7) && |
| !bit_test(e->month, 8) && !bit_test(e->month, 10) && |
| !bit_test(e->month, 12)) |
| return false; |
| } else if (first_day_of_month == 30) { |
| /* Make sure the only month available isn't February. */ |
| if ((bit_fls(e->month) == 2) && (bit_ffs(e->month) == 2)) |
| return false; |
| } |
| |
| return true; |
| } |
| |
| extern char *cronspec_from_cron_entry(cron_entry_t *entry) |
| { |
| char *cronspec = NULL; |
| char *fmt; |
| |
| if (entry->flags & CRON_WILD_MINUTE) { |
| xstrcat(cronspec, "* "); |
| } else { |
| fmt = bit_fmt_full(entry->minute); |
| xstrfmtcat(cronspec, "%s ", fmt); |
| xfree(fmt); |
| } |
| |
| if (entry->flags & CRON_WILD_HOUR) { |
| xstrcat(cronspec, "* "); |
| } else { |
| fmt = bit_fmt_full(entry->hour); |
| xstrfmtcat(cronspec, "%s ", fmt); |
| xfree(fmt); |
| } |
| |
| if (entry->flags & CRON_WILD_DOM) { |
| xstrcat(cronspec, "* "); |
| } else { |
| fmt = bit_fmt_full(entry->day_of_month); |
| xstrfmtcat(cronspec, "%s ", fmt); |
| xfree(fmt); |
| } |
| |
| if (entry->flags & CRON_WILD_MONTH) { |
| xstrcat(cronspec, "* "); |
| } else { |
| fmt = bit_fmt_full(entry->month); |
| xstrfmtcat(cronspec, "%s ", fmt); |
| xfree(fmt); |
| } |
| |
| if (entry->flags & CRON_WILD_DOW) { |
| xstrcat(cronspec, "*"); |
| } else { |
| fmt = bit_fmt_full(entry->day_of_week); |
| xstrfmtcat(cronspec, "%s", fmt); |
| xfree(fmt); |
| } |
| |
| return cronspec; |
| } |
| |
| /* |
| * Determine how many months there are between now and the next month this job |
| * could run in. |
| * |
| * One important note: struct tm has jan == 0, but the crontab |
| * format and our bitstring have jan == 1. |
| */ |
| static int _next_month(cron_entry_t *entry, struct tm *tm) |
| { |
| int months_to_advance = 0; |
| |
| /* tm_mon should be 0-11 */ |
| xassert(tm->tm_mon >= 0); |
| xassert(tm->tm_mon <= 11); |
| |
| /* month is current valid, nice and easy, no major adjustments needed */ |
| if (entry->flags & CRON_WILD_MONTH || |
| bit_test(entry->month, tm->tm_mon + 1)) |
| return 0; |
| |
| /* Start testing from now to get the closest month */ |
| for (int i = tm->tm_mon; i < 12; i++) { |
| if (bit_test(entry->month, i + 1)) |
| goto found; |
| months_to_advance++; |
| } |
| |
| /* Loop around to beginning of the year if needed */ |
| for (int i = 0; i < tm->tm_mon; i++) { |
| if (bit_test(entry->month, i + 1)) |
| goto found; |
| months_to_advance++; |
| } |
| |
| fatal("Could not find a valid month, this should be impossible"); |
| |
| found: |
| /* |
| * Next usable month is not this month. Reset other timing to midnight |
| * on the first of the next valid month. |
| */ |
| tm->tm_mon += months_to_advance; |
| tm->tm_hour = 0; |
| tm->tm_min = 0; |
| tm->tm_mday = 1; |
| slurm_mktime(tm); |
| |
| return 0; |
| } |
| |
| /* |
| * Determine how many days there are between now and the next day of the week |
| * this job could run on. |
| * |
| * Intended for use when day of week is specified in the cron entry. |
| */ |
| static int _next_day_of_week(cron_entry_t *entry, struct tm *tm) |
| { |
| int days_to_advance = 0; |
| |
| /* tm_wday should be 0-6 */ |
| xassert(tm->tm_wday >= 0); |
| xassert(tm->tm_wday <= 6); |
| |
| /* Start testing from now to get the closest day */ |
| for (int i = tm->tm_wday; i < 7; i++) { |
| if (bit_test(entry->day_of_week, i)) |
| return days_to_advance; |
| days_to_advance++; |
| } |
| |
| /* Loop around to beginning of the week if needed */ |
| for (int i = 0; i < tm->tm_wday; i++) { |
| if (bit_test(entry->day_of_week, i)) |
| return days_to_advance; |
| days_to_advance++; |
| } |
| |
| return 0; |
| } |
| |
| /* Return number of days in a given tm->tm_mon */ |
| static int _days_in_month(struct tm *tm) |
| { |
| /* Default to maximum days in month (highest likelihood) */ |
| int days_in_month = 31; |
| |
| /* tm_mon should be 0-11 */ |
| xassert(tm->tm_mon >= 0); |
| xassert(tm->tm_mon <= 11); |
| |
| switch (tm->tm_mon) { |
| case 1: |
| days_in_month = LEAP_YEAR(tm->tm_year) ? 29 : 28; |
| break; |
| case 3: |
| case 5: |
| case 8: |
| case 10: |
| days_in_month = 30; |
| break; |
| } |
| |
| return days_in_month; |
| } |
| |
| /* |
| * Determine how many days there are between now and the next day of the month |
| * this job could run on. |
| * |
| * Intended for use when day of month is specified in the cron entry. |
| */ |
| static int _next_day_of_month(cron_entry_t *entry, struct tm *tm) |
| { |
| int days_to_advance = 0; |
| int days_in_month; |
| |
| /* tm_mday should be 1-31 */ |
| xassert(tm->tm_mday >= 1); |
| xassert(tm->tm_mday <= 31); |
| |
| days_in_month = _days_in_month(tm); |
| |
| /* |
| * Advance days within tm month till cron entry day found (and return |
| * count) or reach days in tm month. |
| */ |
| for (int i = tm->tm_mday; i <= days_in_month; i++) { |
| if (bit_test(entry->day_of_month, i)) |
| return days_to_advance; |
| days_to_advance++; |
| } |
| |
| /* |
| * Continue advancing days within next month till tm_mday found and return |
| * aggregated count of advanced days. |
| */ |
| for (int i = 1; i < tm->tm_mday; i++) { |
| if (bit_test(entry->day_of_month, i)) |
| return days_to_advance; |
| days_to_advance++; |
| } |
| |
| return days_to_advance; |
| } |
| |
| extern time_t calc_next_cron_start(cron_entry_t *entry, time_t next) |
| { |
| struct tm tm; |
| time_t now = time(NULL); |
| int validated_month, days_to_add; |
| |
| /* |
| * Avoid running twice in the same minute. |
| */ |
| if (next && next > now + 60) { |
| now = next; |
| localtime_r(&now, &tm); |
| tm.tm_sec = 0; |
| } else { |
| localtime_r(&now, &tm); |
| tm.tm_sec = 0; |
| tm.tm_min++; |
| } |
| |
| month: |
| _next_month(entry, &tm); |
| |
| validated_month = tm.tm_mon; |
| |
| days_to_add = 0; |
| if ((entry->flags & CRON_WILD_DOM) && (entry->flags & CRON_WILD_DOW)) { |
| /* Wildcard for both is the easy path out. */ |
| ; |
| } else if (entry->flags & CRON_WILD_DOM) { |
| /* |
| * Only pay attention to the day of week. |
| */ |
| days_to_add = _next_day_of_week(entry, &tm); |
| } else if (entry->flags & CRON_WILD_DOW) { |
| /* |
| * Only attention to the day of month. |
| */ |
| days_to_add = _next_day_of_month(entry, &tm); |
| } else { |
| /* |
| * When both are specified, the defacto behavior is to |
| * treat them as OR'd rather than AND'd, as trying to |
| * resolve both simultaneously would result in the job |
| * very rarely running. |
| * So find the soonest time between them and use that. |
| */ |
| int dom_next = _next_day_of_month(entry, &tm); |
| int dow_next = _next_day_of_week(entry, &tm); |
| |
| days_to_add = MIN(dom_next, dow_next); |
| } |
| if (days_to_add) { |
| tm.tm_mday += days_to_add; |
| tm.tm_hour = 0; |
| tm.tm_min = 0; |
| slurm_mktime(&tm); |
| |
| /* month slipped back, need to re-validate */ |
| if (validated_month != tm.tm_mon) |
| goto month; |
| } |
| |
| hour: |
| if (!(entry->flags & CRON_WILD_HOUR) && |
| !bit_test(entry->hour, tm.tm_hour)) { |
| /* must be in future, reset minutes */ |
| tm.tm_min = 0; |
| |
| while (tm.tm_hour < 24) { |
| if (bit_test(entry->hour, tm.tm_hour)) |
| break; |
| tm.tm_hour++; |
| } |
| if (tm.tm_hour == 24) { |
| /* |
| * tm_hour set to 24 rolls the day and possibly |
| * the month at well. revalidate month + day. |
| */ |
| slurm_mktime(&tm); |
| goto month; |
| } |
| } |
| |
| if (!(entry->flags & CRON_WILD_MINUTE) && |
| !bit_test(entry->minute, tm.tm_min)) { |
| while (tm.tm_min < 60) { |
| if (bit_test(entry->minute, tm.tm_min)) |
| break; |
| tm.tm_min++; |
| } |
| if (tm.tm_min == 60 && tm.tm_hour == 23) { |
| /* |
| * this will roll into the next day, |
| * which may also be a new month |
| */ |
| slurm_mktime(&tm); |
| goto month; |
| } else if (tm.tm_min == 60) { |
| /* |
| * next hour, but fortunately still |
| * in the same day |
| */ |
| tm.tm_min = 0; |
| tm.tm_hour++; |
| goto hour; |
| } |
| } |
| |
| return slurm_mktime(&tm); |
| } |
| |
| extern void pack_cron_entry(void *in, uint16_t protocol_version, |
| buf_t *buffer) |
| { |
| uint8_t set = (in ? 1 : 0); |
| cron_entry_t *entry = (cron_entry_t *) in; |
| |
| pack8(set, buffer); |
| |
| if (!set) |
| return; |
| |
| if (protocol_version >= SLURM_MIN_PROTOCOL_VERSION) { |
| pack32(entry->flags, buffer); |
| pack_bit_str_hex(entry->minute, buffer); |
| pack_bit_str_hex(entry->hour, buffer); |
| pack_bit_str_hex(entry->day_of_month, buffer); |
| pack_bit_str_hex(entry->month, buffer); |
| pack_bit_str_hex(entry->day_of_week, buffer); |
| packstr(entry->cronspec, buffer); |
| /* command is not packed, only in struct for parsing */ |
| pack32(entry->line_start, buffer); |
| pack32(entry->line_end, buffer); |
| } |
| } |
| |
| extern int unpack_cron_entry(void **entry_ptr, uint16_t protocol_version, |
| buf_t *buffer) |
| { |
| uint8_t set; |
| cron_entry_t *entry = NULL; |
| |
| xassert(entry_ptr); |
| |
| safe_unpack8(&set, buffer); |
| |
| if (!set) |
| return SLURM_SUCCESS; |
| |
| entry = xmalloc(sizeof(*entry)); |
| *entry_ptr = entry; |
| |
| if (protocol_version >= SLURM_MIN_PROTOCOL_VERSION) { |
| safe_unpack32(&entry->flags, buffer); |
| unpack_bit_str_hex(&entry->minute, buffer); |
| unpack_bit_str_hex(&entry->hour, buffer); |
| unpack_bit_str_hex(&entry->day_of_month, buffer); |
| unpack_bit_str_hex(&entry->month, buffer); |
| unpack_bit_str_hex(&entry->day_of_week, buffer); |
| safe_unpackstr(&entry->cronspec, buffer); |
| /* command is not packed, only in struct for parsing */ |
| safe_unpack32(&entry->line_start, buffer); |
| safe_unpack32(&entry->line_end, buffer); |
| } else { |
| goto unpack_error; |
| } |
| |
| return SLURM_SUCCESS; |
| |
| unpack_error: |
| *entry_ptr = NULL; |
| free_cron_entry(entry); |
| return SLURM_ERROR; |
| } |