/* SPDX-License-Identifier: LGPL-2.1-or-later */
/* systemd service to wait until kernel realtime clock is synchronized */

#include <errno.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/inotify.h>
#include <sys/timerfd.h>
#include <sys/timex.h>
#include <unistd.h>

#include "sd-event.h"

#include "fd-util.h"
#include "inotify-util.h"
#include "main-func.h"
#include "signal-util.h"
#include "time-util.h"

typedef struct ClockState {
        int timerfd_fd;                  /* non-negative is descriptor from timerfd_create */
        int adjtime_state;               /* return value from last adjtimex(2) call */
        sd_event_source *timerfd_event_source; /* non-null is the active io event source */
        int inotify_fd;
        sd_event_source *inotify_event_source;
        int run_systemd_wd;
        int run_systemd_timesync_wd;
        bool has_watchfile;
} ClockState;

static void clock_state_release_timerfd(ClockState *sp) {
        sp->timerfd_event_source = sd_event_source_unref(sp->timerfd_event_source);
        sp->timerfd_fd = safe_close(sp->timerfd_fd);
}

static void clock_state_release(ClockState *sp) {
        clock_state_release_timerfd(sp);
        sp->inotify_event_source = sd_event_source_unref(sp->inotify_event_source);
        sp->inotify_fd = safe_close(sp->inotify_fd);
}

static int clock_state_update(ClockState *sp, sd_event *event);

static int update_notify_run_systemd_timesync(ClockState *sp) {
        sp->run_systemd_timesync_wd = inotify_add_watch(sp->inotify_fd, "/run/systemd/timesync", IN_CREATE|IN_DELETE_SELF);
        return sp->run_systemd_timesync_wd;
}

static int timerfd_handler(sd_event_source *s,
                           int fd,
                           uint32_t revents,
                           void *userdata) {
        ClockState *sp = userdata;

        return clock_state_update(sp, sd_event_source_get_event(s));
}

static void process_inotify_event(sd_event *event, ClockState *sp, struct inotify_event *e) {
        if (e->wd == sp->run_systemd_wd) {
                /* Only thing we care about is seeing if we can start watching /run/systemd/timesync. */
                if (sp->run_systemd_timesync_wd < 0)
                        update_notify_run_systemd_timesync(sp);
        } else if (e->wd == sp->run_systemd_timesync_wd) {
                if (e->mask & IN_DELETE_SELF) {
                        /* Somebody removed /run/systemd/timesync. */
                        (void) inotify_rm_watch(sp->inotify_fd, sp->run_systemd_timesync_wd);
                        sp->run_systemd_timesync_wd = -1;
                } else
                        /* Somebody might have created /run/systemd/timesync/synchronized. */
                        clock_state_update(sp, event);
        }
}

static int inotify_handler(sd_event_source *s,
                           int fd,
                           uint32_t revents,
                           void *userdata) {
        sd_event *event = sd_event_source_get_event(s);
        ClockState *sp = userdata;
        union inotify_event_buffer buffer;
        ssize_t l;

        l = read(fd, &buffer, sizeof(buffer));
        if (l < 0) {
                if (ERRNO_IS_TRANSIENT(errno))
                        return 0;

                return log_warning_errno(errno, "Lost access to inotify: %m");
        }
        FOREACH_INOTIFY_EVENT_WARN(e, buffer, l)
                process_inotify_event(event, sp, e);

        return 0;
}

static int clock_state_update(
                ClockState *sp,
                sd_event *event) {

        struct timex tx = {};
        usec_t t;
        int r;

        clock_state_release_timerfd(sp);

        /* The kernel supports cancelling timers whenever its realtime clock is "set" (which can happen in a variety of
         * ways, generally adjustments of at least 500 ms). The way this module works is we set up a timerfd that will
         * wake when the clock is set, and when that happens we read the clock synchronization state from the return
         * value of adjtimex(2), which supports the NTP time adjustment protocol.
         *
         * The kernel determines whether the clock is synchronized using driver-specific tests, based on time
         * information passed by an application, generally through adjtimex(2). If the application asserts the clock is
         * synchronized, but does not also do something that "sets the clock", the timer will not be cancelled and
         * synchronization will not be detected.
         *
         * Similarly, this service will never complete if the application sets the time without also providing
         * information that adjtimex(2) can use to determine that the clock is synchronized. This generally doesn't
         * happen, but can if the system has a hardware clock that is accurate enough that the adjustment is too small
         * to be a "set".
         *
         * Both these failure-to-detect situations are covered by having the presence/creation of
         * /run/systemd/timesync/synchronized, which is considered sufficient to indicate a synchronized clock even if
         * the kernel has not been updated.
         *
         * For timesyncd the initial setting of the time uses settimeofday(2), which sets the clock but does not mark
         * it synchronized. When an NTP source is selected it sets the clock again with clock_adjtime(2) which marks it
         * synchronized and also touches /run/systemd/timesync/synchronized which covers the case when the clock wasn't
         * "set". */

        r = time_change_fd();
        if (r < 0) {
                log_error_errno(r, "Failed to create timerfd: %m");
                goto finish;
        }
        sp->timerfd_fd = r;

        r = adjtimex(&tx);
        if (r < 0) {
                log_error_errno(errno, "Failed to read adjtimex state: %m");
                goto finish;
        }
        sp->adjtime_state = r;

        if (tx.status & STA_NANO)
                tx.time.tv_usec /= 1000;
        t = timeval_load(&tx.time);

        log_info("adjtime state %i status %x time %s", sp->adjtime_state, (unsigned) tx.status,
                 FORMAT_TIMESTAMP_STYLE(t, TIMESTAMP_US_UTC) ?: "unrepresentable");

        sp->has_watchfile = access("/run/systemd/timesync/synchronized", F_OK) >= 0;
        if (sp->has_watchfile)
                /* Presence of watch file overrides adjtime_state */
                r = 0;
        else if (sp->adjtime_state == TIME_ERROR) {
                /* Not synchronized.  Do a one-shot wait on the descriptor and inform the caller we need to keep
                 * running. */
                r = sd_event_add_io(event, &sp->timerfd_event_source, sp->timerfd_fd,
                                    EPOLLIN, timerfd_handler, sp);
                if (r < 0) {
                        log_error_errno(r, "Failed to create time change monitor source: %m");
                        goto finish;
                }
                r = 1;
        } else
                /* Synchronized; we can exit. */
                r = 0;

 finish:
        if (r <= 0)
                (void) sd_event_exit(event, r);
        return r;
}

static int run(int argc, char * argv[]) {
        _cleanup_(sd_event_unrefp) sd_event *event = NULL;
        _cleanup_(clock_state_release) ClockState state = {
                .timerfd_fd = -EBADF,
                .inotify_fd = -EBADF,
                .run_systemd_wd = -1,
                .run_systemd_timesync_wd = -1,
        };
        int r;

        r = sd_event_default(&event);
        if (r < 0)
                return log_error_errno(r, "Failed to allocate event loop: %m");

        r = sd_event_set_signal_exit(event, true);
        if (r < 0)
                return log_error_errno(r, "Failed to enable SIGTERM/SIGINT handling: %m");

        r = sd_event_set_watchdog(event, true);
        if (r < 0)
                return log_error_errno(r, "Failed to create watchdog event source: %m");

        r = inotify_init1(IN_NONBLOCK|IN_CLOEXEC);
        if (r < 0)
                return log_error_errno(errno, "Failed to create inotify descriptor: %m");

        state.inotify_fd = r;

        r = sd_event_add_io(event, &state.inotify_event_source, state.inotify_fd,
                            EPOLLIN, inotify_handler, &state);
        if (r < 0)
                return log_error_errno(r, "Failed to create notify event source: %m");

        r = inotify_add_watch_and_warn(state.inotify_fd, "/run/systemd/", IN_CREATE);
        if (r < 0)
                return r;

        state.run_systemd_wd = r;

        (void) update_notify_run_systemd_timesync(&state);

        r = clock_state_update(&state, event);
        if (r > 0) {
                r = sd_event_loop(event);
                if (r < 0)
                        log_error_errno(r, "Failed in event loop: %m");
        }

        if (state.has_watchfile)
                log_debug("Exit enabled by: /run/systemd/timesync/synchronized");

        if (state.adjtime_state == TIME_ERROR)
                log_info("Exit without adjtimex synchronized.");

        return r;
}

DEFINE_MAIN_FUNCTION(run);
