diff --git a/man/systemd.time.xml b/man/systemd.time.xml index 820e5e499ec..6dbf5db7341 100644 --- a/man/systemd.time.xml +++ b/man/systemd.time.xml @@ -255,6 +255,13 @@ tomorrow Pacific/Auckland → Thu 2012-11-23 19:00:00 the local timezone, similar to the supported syntax of timestamps (see above), or the timezone in the IANA timezone database format (also see above). + There is a special format for periodic events in the form + «timestamp» and every «timespan» to describe general + periodic intervals. For example timespan 2w, 3w, 2d… + The reference point for such an interval must be a single point in time + (timestamp). This syntax can't be combined with previously described wild-card + formats. The minimum value for time period is 60s. + The following special expressions may be used as shorthands for longer normalized forms: minutely → *-*-* *:*:00 @@ -302,7 +309,8 @@ Wed..Sat,Tue 12-10-15 1:2:3 → Tue..Sat 2012-10-15 01:02:03 weekly Pacific/Auckland → Mon *-*-* 00:00:00 Pacific/Auckland yearly → *-01-01 00:00:00 annually → *-01-01 00:00:00 - *:2/3 → *-*-* *:02/3:00 + *:2/3 → *-*-* *:02/3:00 + 25-1-1 10 CET and every 2w → 2025-01-01 09:00:00 UTC and every 2w Calendar events are used by timer units, see systemd.timer5 diff --git a/src/shared/calendarspec.c b/src/shared/calendarspec.c index 5fdae235366..0a51b887d38 100644 --- a/src/shared/calendarspec.c +++ b/src/shared/calendarspec.c @@ -26,6 +26,7 @@ #define BITS_WEEKDAYS 127 #define MIN_YEAR 1970 #define MAX_YEAR 2199 +#define MIN_PERIOD (60 * USEC_PER_SEC) /* An arbitrary limit on the length of the chains of components. We don't want to * build a very long linked list, which would be slow to iterate over and might cause @@ -382,6 +383,13 @@ int calendar_spec_to_string(const CalendarSpec *c, char **ret) { } } + if (c->period) { + char buf[FORMAT_TIMESPAN_MAX]; + fputs(" and every ", f); + (void) format_timespan(buf, sizeof(buf), c->period, 0); + fputs(buf, f); + } + return memstream_finalize(&m, ret, NULL); } @@ -620,6 +628,39 @@ static int calendarspec_from_time_t(CalendarSpec *c, time_t time) { return 0; } +static bool is_single_value_component(CalendarComponent *cc) { + assert(cc); + return cc->stop == -1 && cc->repeat == 0 && cc->next == NULL; +} + +static int calendarspec_to_usec_t(const CalendarSpec *c, usec_t *t) { + struct tm tm; + int r; + + assert(c); + assert(is_single_value_component(c->year)); + assert(is_single_value_component(c->month)); + assert(is_single_value_component(c->day)); + assert(is_single_value_component(c->hour)); + assert(is_single_value_component(c->minute)); + assert(is_single_value_component(c->microsecond)); + + tm.tm_year = c->year->start - 1900; + tm.tm_mon = c->month->start - 1; + tm.tm_mday = c->day->start; + tm.tm_hour = c->hour->start; + tm.tm_min = c->minute->start; + tm.tm_sec = c->microsecond->start / USEC_PER_SEC; + tm.tm_isdst = -1; + + r = mktime_or_timegm_usec(&tm, c->utc, t); + if (r < 0) + return r; + if (t) + *t += c->microsecond->start % USEC_PER_SEC; + return 0; +} + static int prepend_component(const char **p, bool usec, unsigned nesting, CalendarComponent **c) { int r, start, stop = -1, repeat = 0; CalendarComponent *cc; @@ -872,6 +913,38 @@ finish: return 0; } +static int parse_and_every(const char *s, usec_t *ts, usec_t *rep) { + _cleanup_free_ char *p_ts = NULL; + int i, r; + + char const *p = strcasestr(s, " and "); + if ( !p ) + return -EINVAL; + + p_ts = strndup(s, p - s); + i = strlen(p_ts); + while ( i > 0 && isspace(p_ts[--i]) ) + p_ts[i] = (char)0; + + r = parse_timestamp(p_ts, ts); + if (r < 0) + return r; + + p += strspn(p, " "); + if ( !(p = startswith_no_case(p, "and ")) ) + return -EINVAL; + p += strspn(p, " "); + if ( !(p = startswith_no_case(p, "every ")) ) + return -EINVAL; + p += strspn(p, " "); + + r = parse_time(p, rep, USEC_PER_SEC); + if (r < 0) + return r; + + return 0; +} + int calendar_spec_from_string(const char *p, CalendarSpec **ret) { const char *utc; _cleanup_(calendar_spec_freep) CalendarSpec *c = NULL; @@ -889,6 +962,21 @@ int calendar_spec_from_string(const char *p, CalendarSpec **ret) { .timezone = NULL, }; + if (strcasestr(p, " and ")) { + usec_t ts; + r = parse_and_every(p, &ts, &(c->period)); + if (r < 0) + return r; + if (c->period < MIN_PERIOD) + return -ERANGE; + r = calendarspec_from_time_t(c, (time_t)(ts / USEC_PER_SEC)); + if (r < 0) + return r; + c->microsecond->start += ts % USEC_PER_SEC; + c->weekdays_bits = -1; + goto finish; + } + utc = endswith_no_case(p, " UTC"); if (utc) { c->utc = true; @@ -1089,6 +1177,7 @@ int calendar_spec_from_string(const char *p, CalendarSpec **ret) { calendar_spec_normalize(c); +finish: if (!calendar_spec_valid(c)) return -EINVAL; @@ -1359,6 +1448,67 @@ static int find_next(const CalendarSpec *spec, struct tm *tm, usec_t *usec) { "Infinite loop in calendar calculation: %s", strna(s)); } +static int usec_dstoffset_sec(usec_t usec, time_t *ret_dstoffset) { + struct tm tm; + time_t t = usec / USEC_PER_SEC; + if ( !localtime_r(&t, &tm) ) + return -EINVAL; + if ( tm.tm_isdst) { + tm.tm_isdst = 0; + time_t tmp = mktime(&tm); + if ( tmp == -1 ) + return -EINVAL; + *ret_dstoffset = tmp - t; + } else { + *ret_dstoffset = 0; + } + return 0; +} + +static int calendar_spec_next_usec_period(const CalendarSpec *spec, usec_t usec, usec_t *ret_next) { + usec_t ts; + int r = calendarspec_to_usec_t(spec, &ts); + if (r < 0) + return r; + if (usec < ts) { + /* The timestamp is in the future. The first event of the period. */ + *ret_next = ts; + return 0; + } + + if ( spec->period % (24 * 3600 * USEC_PER_SEC) ) { + /* The period is not multiple of the whole day. + * The next event is on the multiple of the period duration. */ + *ret_next = ts + ( (usec - ts) / spec->period +1 ) * spec->period; + return 0; + } + + /* The period is multiple of the whole day. We need to compensate daylight + * saving. The repeating events should be at the same time of day according to + * daylight saving. We need to test up to three points around and detect + * localtime discontinuity on change of DST/Non DST localtime mode. */ + time_t dstoffset; + if ( (r = usec_dstoffset_sec(ts, &dstoffset)) ) + return r; + const size_t NRT = 3; + size_t i; + usec_t u = ts + ( (usec - ts) / spec->period ) * spec->period; + for(i = 0; i < NRT; i++) { + time_t dstoffset2; + r = usec_dstoffset_sec(u, &dstoffset2); + if (r < 0) + return r; + u += ( dstoffset - dstoffset2 ) * USEC_PER_SEC; + if ( usec < u ) { + *ret_next = u; + return 0; + } + dstoffset = dstoffset2; + u += spec->period; + } + return -EINVAL; +} + static int calendar_spec_next_usec_impl(const CalendarSpec *spec, usec_t usec, usec_t *ret_next) { usec_t tm_usec; struct tm tm; @@ -1369,6 +1519,9 @@ static int calendar_spec_next_usec_impl(const CalendarSpec *spec, usec_t usec, u if (usec > USEC_TIMESTAMP_FORMATTABLE_MAX) return -EINVAL; + if ( spec->period ) + return calendar_spec_next_usec_period(spec, usec, ret_next); + usec++; r = localtime_or_gmtime_usec(usec, spec->utc, &tm); if (r < 0) diff --git a/src/shared/calendarspec.h b/src/shared/calendarspec.h index 60c1c792673..a5d1d910155 100644 --- a/src/shared/calendarspec.h +++ b/src/shared/calendarspec.h @@ -22,6 +22,7 @@ typedef struct CalendarSpec { bool utc:1; signed int dst:2; char *timezone; + usec_t period; CalendarComponent *year; CalendarComponent *month; diff --git a/src/test/test-calendarspec.c b/src/test/test-calendarspec.c index 0fc3a43a23b..9e27956d40d 100644 --- a/src/test/test-calendarspec.c +++ b/src/test/test-calendarspec.c @@ -181,6 +181,7 @@ TEST(calendar_spec_one) { test_one("@0 UTC", "1970-01-01 00:00:00 UTC"); test_one("*:05..05", "*-*-* *:05:00"); test_one("*:05..10/6", "*-*-* *:05:00"); + test_one("2025-3-1 10:0 UTC and every 14d", "2025-03-01 10:00:00 UTC and every 2w"); } TEST(calendar_spec_next) {