/*
* Copyright 2024 GNOME Foundation, Inc.
*
* SPDX-License-Identifier: LGPL-2.1-or-later
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General
* Public License along with this library; if not, see .
*
* Author: Philip Withnall
*/
#include "config.h"
#include
#include
#include
#include
#include "shell-time-change-source.h"
typedef struct
{
GSource source;
int fd; /* (owned) (nullable) */
void *tag; /* (owned) (nullable) */
} ShellTimeChangeSource;
static int
arm_timerfd (int fd)
{
struct itimerspec its = {
/* Get the biggest value we can in the `time_t`, as the timerfd will fire
* spuriously when that time is reached. Unfortunately there is no
* `TIME_T_MAX`. */
.it_value.tv_sec = (sizeof (time_t) >= 8) ? UINT64_MAX : UINT32_MAX,
};
int flags = TFD_TIMER_ABSTIME | TFD_TIMER_CANCEL_ON_SET;
if (timerfd_settime (fd, flags, &its, NULL) == 0)
return 0;
if (errno != EINVAL)
return -1;
/* Try again with a smaller timeout. It’s possible that libc supports
* 64-bit time while the kernel doesn’t. */
its.it_value.tv_sec = UINT32_MAX;
return timerfd_settime (fd, flags, &its, NULL);
}
static void
shell_time_change_source_cleanup_fd (ShellTimeChangeSource *self)
{
/* Make sure the FD is closed. */
if (self->tag != NULL)
{
g_source_remove_unix_fd ((GSource *) self, self->tag);
self->tag = NULL;
}
g_clear_fd (&self->fd, NULL);
}
static gboolean
shell_time_change_source_dispatch (GSource *source,
GSourceFunc callback,
gpointer user_data)
{
ShellTimeChangeSource *self = (ShellTimeChangeSource *) source;
if (callback == NULL)
{
g_warning ("ShellTimeChangeSource dispatched without callback. "
"You must call g_source_set_callback().");
return G_SOURCE_REMOVE;
}
if (callback (user_data))
{
/* The timerfd_settime() call can’t really fail in this situation.
* The man page says it can return ECANCELED, but will still be re-armed. */
int retval = arm_timerfd (self->fd);
int errsv = errno;
g_assert (retval == 0 ||
(retval < 0 && errsv == ECANCELED));
return G_SOURCE_CONTINUE;
}
/* Clean up the source’s resources early, as FDs are precious, and the user
* might leave the source hanging around for a long time before finalising it. */
shell_time_change_source_cleanup_fd (self);
return G_SOURCE_REMOVE;
}
static void
shell_time_change_source_finalize (GSource *source)
{
ShellTimeChangeSource *self = (ShellTimeChangeSource *) source;
shell_time_change_source_cleanup_fd (self);
}
static const GSourceFuncs shell_time_change_source_funcs = {
NULL, /* prepare */
NULL, /* check */
shell_time_change_source_dispatch,
shell_time_change_source_finalize,
NULL, NULL
};
/**
* shell_time_change_source_new:
* @error: return location for a #GError, or %NULL
*
* Creates a #GSource which is dispatched every time the system realtime clock
* changes relative to the monotonic clock.
*
* This typically happens after NTP synchronisation.
*
* On error, a #GFileError will be returned. This happens if a timerfd cannot be
* created.
*
* Any callback attached to the returned #GSource must have type
* #GSourceFunc.
*
* Returns: (transfer full): the newly created #GSource, or %NULL on error
*/
GSource *
shell_time_change_source_new (GError **error)
{
ShellTimeChangeSource *self;
g_autoptr(GSource) source = NULL;
g_autofd int fd = -1;
g_return_val_if_fail (error == NULL || *error == NULL, NULL);
/* Create a timerfd with the maximum possible timeout, but set
* `TFD_TIMER_CANCEL_ON_SET` so that it fires if the realtime clock changes
* relative to the monotonic clock.
*
* This is a one-shot source: it’ll need to be recreated after that. */
fd = timerfd_create (CLOCK_REALTIME, TFD_NONBLOCK | TFD_CLOEXEC);
if (fd < 0 || arm_timerfd (fd) < 0)
{
int errsv = errno;
g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errsv),
"Error creating timerfd: %s", g_strerror (errsv));
return NULL;
}
source = g_source_new ((GSourceFuncs *) &shell_time_change_source_funcs,
sizeof (ShellTimeChangeSource));
self = (ShellTimeChangeSource *) source;
self->tag = g_source_add_unix_fd (source, fd, G_IO_IN);
self->fd = g_steal_fd (&fd);
return g_steal_pointer (&source);
}