2
votes

I was trying to find a way to receive a notification from the system (Linux) when daylight savings are applied, but I do not seem to be able to find anything like that.

Consider a program sits on a pselect() waiting for a number of timer fd's, all which have exactly 24-hour intervals, but differing start times, which are defined by a user; "07:00 ON, 07:25 OFF" (for example, if it were a coffee maker).

Because the user gives these times in local time and Linux runs on UTC, the time zone adjusted timer fd's need to be readjusted each time a daylight savings occure. (user expects coffee when his daylight savings compliant alarm clock has woken him up...)

Intelligent way to go about this, as I would imagine, would be to register to the system/kernel/init/whatever to be notified when daylight savings are applied, and avoid getting into the messy business of trying to determine such dates and times yourself and hope the system agrees with your results (ie. your resync actions and actual daylight savings happen in the same time).

Is there any way to be notified on DST changes? Or perhaps on any changes to local time (assuming DST change modifies that)?

2

2 Answers

1
votes

Consider a program sits on a pselect() waiting for a number of timer fd's, all which have exactly 24-hour intervals, but differing start times

Therein lies your fundamental problem. All days are not exactly 24 hours long -- sometimes they are off by an hour (daylight savings time), or by seconds (leap seconds); just like not every February has 28 days.

A much simpler and lightweight (less resources consumed) way is to use a min-heap of future events in UTC, something like

struct trigger {
    /* Details on how the event is defined;
       for example, "each day at 07:00 local time".
    */
};

struct utc_event {
    struct trigger  *trigger;
    time_t           when;
};

struct event_min_heap {
    size_t           max_events;
    size_t           num_events;
    struct utc_event event[];
};

The event C99 flexible array member in struct event_min_heap is an array with num_events events (memory allocated for max_events; can be reallocated if more events are needed) in a min heap keyed by the when field in each event entry. That is, the earliest event is always at the root.

Whenever current time is at least event[0].when, it is "triggered" -- meaning whatever action is to be taken, is taken --, and based on the struct trigger it refers to, the time of the next occurrence of that event is updated to event[0], then it is percolated down in the heap to its proper place. Note that you simply use mktime() to obtain the UTC time from broken-down local time fields.

(If this were a multi-user service, then you can support multiple concurrent timezones, one for each trigger, by setting the TZ environment variable to the respective timezone definition, and calling tzset() before the call to mktime(). Because the environment is shared by all threads in the process, you would need to ensure only one thread does this at a time, if you have a multithreaded process. Normally, stuff like this is perfectly implementable using a single-threaded process.)

When the event in the root (event[0]) is deleted or percolated (sifted), the event with the next smallest when will be at the root. If when is equal or less to current time in UTC, it too is triggered.

When the next when is in the future, the process can sleep the remaining interval.

That is all there is to it. You don't need multiple timers -- which are a system-wide finite resource --, and you don't need to worry about whether some local time is daylight savings time or not; the C library mktime() will take care of such details for you.


Now, if you don't like this approach (which, again, uses fewer resources than the approach you outlined in your question), contact the SystemD developers. If you kiss up to them obsequiously enough, I'm sure they'll provide a dbus signal for you. It's not like there is any sanity in its current design, and one more wart certainly won't make it any worse. Switching to C# is likely to be considered a plus.


It is crucial to understand that mktime() computes the Unix Epoch time (time_t) for the specified moment, applying daylight savings time if it applies at that specific moment. It does not matter whether daylight savings time is in effect when the function is called!

Also, UTC time is Coordinated Universal Time, and is not subject to timezones or daylight savings time.

Consider the following program, mktime-example.c:

#define  _POSIX_C_SOURCE 200809L
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <time.h>

static time_t epoch(struct tm *const tm,
                    const int year, const int month, const int day,
                    const int hour, const int minute, const int second,
                    const int isdst)
{
    struct tm  temp;
    time_t     result;

    memset(&temp, 0, sizeof temp);
    temp.tm_year = year - 1900;
    temp.tm_mon = month - 1;
    temp.tm_mday = day;
    temp.tm_hour = hour;
    temp.tm_min = minute;
    temp.tm_sec = second;
    temp.tm_isdst = isdst;

    result = mktime(&temp);

    if (isdst >= 0 && isdst != temp.tm_isdst) {
        /* The caller is mistaken about DST, and mktime()
         * adjusted the time. We readjust it. */
        temp.tm_year = year - 1900;
        temp.tm_mon = month - 1;
        temp.tm_mday = day;
        temp.tm_hour = hour;
        temp.tm_min = minute;
        temp.tm_sec = second;
        /* Note: tmp.tm_isdst is kept unchanged. */

        result = mktime(&temp);
    }

    if (tm)
        memcpy(tm, &temp, sizeof temp);

    return result;
}

static void show(const time_t t, const struct tm *const tm)
{
    printf("(time_t)%lld = %04d-%02d-%02d %02d:%02d:%02d",
           (long long)t, tm->tm_year+1900, tm->tm_mon+1, tm->tm_mday,
           tm->tm_hour, tm->tm_min, tm->tm_sec);

    if (tm->tm_isdst == 1)
        printf(", DST in effect");
    else
    if (tm->tm_isdst == 0)
        printf(", DST not in effect");
    else
    if (tm->tm_isdst == -1)
        printf(", Unknown if DST in effect");

    if (tzname[0] && tzname[0][0])
        printf(", %s timezone", tzname[0]);

    printf("\n");
    fflush(stdout);
}

int main(int argc, char *argv[])
{
    struct tm  tm;
    time_t     t;
    long long  secs;
    int        arg, year, month, day, hour, min, sec, isdst, n;
    char       ch;

    if (argc < 2 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
        fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv[0]);
        fprintf(stderr, "       %s [ :REGION/CITY | =TIMEZONE ] @EPOCH | YYYYMMDD-HHMMSS[+-] ...\n", argv[0]);
        fprintf(stderr, "Where:\n");
        fprintf(stderr, "       EPOCH is in UTC seconds since 19700101T000000,\n");
        fprintf(stderr, "       + after time indicates you prefer daylight savings time,\n");
        fprintf(stderr, "       - after time indicates you prefer standard time.\n");
        fprintf(stderr, "\n");
        return EXIT_FAILURE;
    }

    for (arg = 1; arg < argc; arg++) {

        if (argv[arg][0] == ':') {
            if (argv[arg][1])
                setenv("TZ", argv[arg], 1);
            else
                unsetenv("TZ");
            tzset();
            continue;
        }

        if (argv[arg][0] == '=') {
            if (argv[arg][1])
                setenv("TZ", argv[arg] + 1, 1);
            else
                unsetenv("TZ");
            tzset();
            continue;
        }

        if (argv[arg][0] == '@') {
            if (sscanf(argv[arg] + 1, " %lld %c", &secs, &ch) == 1) {
                t = (time_t)secs;
                if (localtime_r(&t, &tm)) {
                    show(t, &tm);
                    continue;
                } 
            }
        }

        n = sscanf(argv[arg], " %04d %02d %02d %*[-Tt] %02d %02d %02d %c",
                              &year, &month, &day, &hour, &min, &sec, &ch);
        if (n >= 6) {
            if (n == 6)
                isdst = -1;
            else
            if (ch == '+')
                isdst = +1; /* DST */
            else
            if (ch == '-')
                isdst = 0;  /* Not DST */
            else
                isdst = -1;

            t = epoch(&tm, year, month, day, hour, min, sec, isdst);
            if (t != (time_t)-1) {
                show(t, &tm);
                continue;
            }
        }

        fflush(stdout);
        fprintf(stderr, "%s: Cannot parse parameter.\n", argv[arg]);
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}

Compile it using e.g.

gcc -Wall -O2 mktime-example.c -o mktime-example

Run it without arguments to see the command-line usage. Run

./mktime-example :Europe/Helsinki 20161030-035959+ 20161030-030000- 20161030-030000+ 20161030-035959- 20161030-040000-

to examine the Unix timestamps around the time when DST ends in 2016 in Helsinki, Finland. The command will output

(time_t)1477789199 = 2016-10-30 03:59:59, DST in effect, EET timezone
(time_t)1477789200 = 2016-10-30 03:00:00, DST not in effect, EET timezone
(time_t)1477785600 = 2016-10-30 03:00:00, DST in effect, EET timezone
(time_t)1477792799 = 2016-10-30 03:59:59, DST not in effect, EET timezone
(time_t)1477792800 = 2016-10-30 04:00:00, DST not in effect, EET timezone

The output will be the same regardless of whether at the time of running this DST is in effect in some timezone or not!

When calling mktime() with .tm_isdst = 0 or .tm_isdst = 1, and mktime() changes it, it also changes the time specified (by the daylight savings time). When .tm_isdst = -1, it means caller is unaware of whether DST is applied or not, and the library will find out; but if there is both a valid standard time and DST time, the C library will pick one (you should assume it does so randomly). The epoch() function above corrects for this when necessary, un-adjusting the time if the user is not correct about DST.

1
votes

Unix/linux systems only deal with UTC, and they use the time_t data (the number of seconds since 00:00h jan, 1st of 1970 UTC till now) as the internal time. Conversions to local time (with the complexities due to exceptions, variations for summer-winter periods, etc.) is done only when displaying the information to the user, so only on converting to local time it is done. As said, no provision to schedule something or preparation for it is made in the unix system.

From zdump(1) you can get all the info you want, per timezone, and use it to construct a crontab to notify you when the switch is to be made. It consults the local database of timezones and extracts all the info about switching (including historic) from winter to summer or the reverse.

$ zdump -v Europe/Madrid
Europe/Madrid  Fri Dec 13 20:45:52 1901 UTC = Fri Dec 13 20:45:52 1901 WET isdst=0 gmtoff=0
Europe/Madrid  Sat Dec 14 20:45:52 1901 UTC = Sat Dec 14 20:45:52 1901 WET isdst=0 gmtoff=0
Europe/Madrid  Sat May  5 22:59:59 1917 UTC = Sat May  5 22:59:59 1917 WET isdst=0 gmtoff=0
Europe/Madrid  Sat May  5 23:00:00 1917 UTC = Sun May  6 00:00:00 1917 WEST isdst=1 gmtoff=3600
Europe/Madrid  Sat Oct  6 22:59:59 1917 UTC = Sat Oct  6 23:59:59 1917 WEST isdst=1 gmtoff=3600
Europe/Madrid  Sat Oct  6 23:00:00 1917 UTC = Sat Oct  6 23:00:00 1917 WET isdst=0 gmtoff=0
Europe/Madrid  Mon Apr 15 22:59:59 1918 UTC = Mon Apr 15 22:59:59 1918 WET isdst=0 gmtoff=0
Europe/Madrid  Mon Apr 15 23:00:00 1918 UTC = Tue Apr 16 00:00:00 1918 WEST isdst=1 gmtoff=3600
Europe/Madrid  Sun Oct  6 22:59:59 1918 UTC = Sun Oct  6 23:59:59 1918 WEST isdst=1 gmtoff=3600
Europe/Madrid  Sun Oct  6 23:00:00 1918 UTC = Sun Oct  6 23:00:00 1918 WET isdst=0 gmtoff=0
Europe/Madrid  Sat Apr  5 22:59:59 1919 UTC = Sat Apr  5 22:59:59 1919 WET isdst=0 gmtoff=0
Europe/Madrid  Sat Apr  5 23:00:00 1919 UTC = Sun Apr  6 00:00:00 1919 WEST isdst=1 gmtoff=3600
Europe/Madrid  Mon Oct  6 22:59:59 1919 UTC = Mon Oct  6 23:59:59 1919 WEST isdst=1 gmtoff=3600
Europe/Madrid  Mon Oct  6 23:00:00 1919 UTC = Mon Oct  6 23:00:00 1919 WET isdst=0 gmtoff=0
Europe/Madrid  Wed Apr 16 22:59:59 1924 UTC = Wed Apr 16 22:59:59 1924 WET isdst=0 gmtoff=0
Europe/Madrid  Wed Apr 16 23:00:00 1924 UTC = Thu Apr 17 00:00:00 1924 WEST isdst=1 gmtoff=3600
Europe/Madrid  Sat Oct  4 22:59:59 1924 UTC = Sat Oct  4 23:59:59 1924 WEST isdst=1 gmtoff=3600
Europe/Madrid  Sat Oct  4 23:00:00 1924 UTC = Sat Oct  4 23:00:00 1924 WET isdst=0 gmtoff=0
Europe/Madrid  Sat Apr 17 22:59:59 1926 UTC = Sat Apr 17 22:59:59 1926 WET isdst=0 gmtoff=0
Europe/Madrid  Sat Apr 17 23:00:00 1926 UTC = Sun Apr 18 00:00:00 1926 WEST isdst=1 gmtoff=3600
Europe/Madrid  Sat Oct  2 22:59:59 1926 UTC = Sat Oct  2 23:59:59 1926 WEST isdst=1 gmtoff=3600
Europe/Madrid  Sat Oct  2 23:00:00 1926 UTC = Sat Oct  2 23:00:00 1926 WET isdst=0 gmtoff=0
Europe/Madrid  Sat Apr  9 22:59:59 1927 UTC = Sat Apr  9 22:59:59 1927 WET isdst=0 gmtoff=0
...

By the way, if you want to be advised of an imminent localtime change, you can use the previous info to construct a crontab file, including all the info, or simply construct a crontab file that include the rules that apply at your localty. For example, if I want to be advised one day before a switch change in Spain (it changes on last sunday of march/october, at 02/03h) you can add some rules in your crontab file:

0 0 24-30 3,10 5 echo Time daylight savings change scheduled for tomorrow | mail [email protected]

and a mail will be sent to you in on every saturday(5) that happens to be in the week from 24-30th of march and october (3,10 part) of each year at 00:00h (localtime). I'm sure you'll be able to adapt this example to your localty or time of advance (so, the day before a time change happens).