summaryrefslogtreecommitdiff
path: root/src/rcl-time-utils.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/rcl-time-utils.c')
-rw-r--r--src/rcl-time-utils.c686
1 files changed, 686 insertions, 0 deletions
diff --git a/src/rcl-time-utils.c b/src/rcl-time-utils.c
new file mode 100644
index 0000000..37fae68
--- /dev/null
+++ b/src/rcl-time-utils.c
@@ -0,0 +1,686 @@
+
+/*
+ * Copyright (C) 2023 Andrey V.Kosteltsev <kx@radix.pro>
+ *
+ * This program 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.
+ *
+ * This program 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 this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ */
+
+#include "rcl-time-utils.h"
+
+
+#ifndef ARRAY_SIZE
+# define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
+#endif
+
+
+static const char *rtc_dev_name;
+static int rtc_dev_fd = -1;
+
+/***************************************************************
+ Static RTC functions:
+ */
+static void close_rtc( void )
+{
+ if( rtc_dev_fd != 1 )
+ close( rtc_dev_fd );
+ rtc_dev_fd = -1;
+}
+
+static int open_rtc( void )
+{
+ static const char *fls[] = {
+ "/dev/rtc0",
+ "/dev/rtc",
+ "/dev/misc/rtc"
+ };
+ size_t i;
+
+ if( rtc_dev_fd != -1 )
+ return rtc_dev_fd;
+
+ if( rtc_dev_name )
+ {
+ rtc_dev_fd = open( rtc_dev_name, O_RDONLY );
+ }
+ else
+ {
+ for( i = 0; i < ARRAY_SIZE(fls); ++i )
+ {
+ rtc_dev_fd = open( fls[i], O_RDONLY );
+
+ if( rtc_dev_fd < 0 )
+ {
+ if( errno == ENOENT || errno == ENODEV )
+ continue;
+ }
+ rtc_dev_name = fls[i];
+ break;
+ }
+ if( rtc_dev_fd < 0 )
+ rtc_dev_name = *fls; /* default for error messages */
+ }
+
+ if( rtc_dev_fd != -1 )
+ atexit( close_rtc );
+
+ return rtc_dev_fd;
+}
+
+/***************************************************************
+ Static functions:
+ */
+static gboolean
+symlink_atomic( const char *target, const char *link_path )
+{
+ if( symlink( target, link_path ) == 0 )
+ return TRUE;
+ else
+ return FALSE;
+}
+
+static gchar *skip_root( const gchar *path )
+{
+ if( !path || *path == '\0' ) return NULL;
+
+ return (gchar *)(path + 1);
+}
+
+/***************************************************************
+ Clock functions:
+ */
+gboolean ntp_synchronized( void )
+{
+ struct timex txc = {};
+
+ if( adjtimex( &txc ) < 0 )
+ return FALSE;
+
+ /*
+ Consider the system clock synchronized if the reported maximum error is
+ smaller than the maximum value (16 seconds). Ignore the STA_UNSYNC flag
+ as it may have been set to prevent the kernel from touching the RTC.
+ */
+ /* return txc.maxerror < 16000000; */
+ return txc.maxerror < 32000000;
+}
+
+
+guint64 timespec_load( const struct timespec *ts )
+{
+ if( !ts )
+ return (guint64)0;
+
+ if( ts->tv_sec < 0 || ts->tv_nsec < 0 )
+ return USEC_INFINITY;
+
+ if( (guint64)ts->tv_sec > (UINT64_MAX - (ts->tv_nsec / NSEC_PER_USEC)) / USEC_PER_SEC )
+ return USEC_INFINITY;
+
+ return
+ (guint64)ts->tv_sec * USEC_PER_SEC +
+ (guint64)ts->tv_nsec / NSEC_PER_USEC;
+}
+
+guint64 timespec_load_nsec( const struct timespec *ts )
+{
+ if( !ts )
+ return (guint64)0;
+
+ if( ts->tv_sec < 0 || ts->tv_nsec < 0 )
+ return NSEC_INFINITY;
+
+ if( (guint64)ts->tv_sec >= (UINT64_MAX - ts->tv_nsec) / NSEC_PER_SEC )
+ return NSEC_INFINITY;
+
+ return (guint64) ts->tv_sec * NSEC_PER_SEC + (guint64)ts->tv_nsec;
+}
+
+struct timespec *timespec_store( struct timespec *ts, guint64 u )
+{
+ if( !ts )
+ return NULL;
+
+ if( u == USEC_INFINITY ||
+ u / USEC_PER_SEC >= TIME_T_MAX )
+ {
+ ts->tv_sec = (time_t) -1;
+ ts->tv_nsec = -1L;
+ return ts;
+ }
+
+ ts->tv_sec = (time_t)(u / USEC_PER_SEC);
+ ts->tv_nsec = (long)((u % USEC_PER_SEC) * NSEC_PER_USEC);
+
+ return ts;
+}
+
+struct timespec *timespec_store_nsec( struct timespec *ts, guint64 n )
+{
+ if( !ts )
+ return NULL;
+
+ if( n == NSEC_INFINITY ||
+ n / NSEC_PER_SEC >= TIME_T_MAX )
+ {
+ ts->tv_sec = (time_t) -1;
+ ts->tv_nsec = -1L;
+ return ts;
+ }
+
+ ts->tv_sec = (time_t)(n / NSEC_PER_SEC);
+ ts->tv_nsec = (long)(n % NSEC_PER_SEC);
+
+ return ts;
+}
+
+guint64 timeval_load( const struct timeval *tv )
+{
+ if( !tv )
+ return (guint64)0;
+
+ if( tv->tv_sec < 0 || tv->tv_usec < 0 )
+ return USEC_INFINITY;
+
+ if( (guint64)tv->tv_sec > (UINT64_MAX - tv->tv_usec) / USEC_PER_SEC )
+ return USEC_INFINITY;
+
+ return
+ (guint64) tv->tv_sec * USEC_PER_SEC +
+ (guint64) tv->tv_usec;
+}
+
+struct timeval *timeval_store( struct timeval *tv, guint64 u )
+{
+ if( !tv )
+ return NULL;
+
+ if( u == USEC_INFINITY ||
+ u / USEC_PER_SEC > TIME_T_MAX )
+ {
+ tv->tv_sec = (time_t) -1;
+ tv->tv_usec = (suseconds_t) -1;
+ }
+ else
+ {
+ tv->tv_sec = (time_t)(u / USEC_PER_SEC);
+ tv->tv_usec = (suseconds_t)(u % USEC_PER_SEC);
+ }
+
+ return tv;
+}
+
+guint64 now( clockid_t clock_id )
+{
+ struct timespec ts;
+
+ if( clock_gettime( CLOCK_REALTIME, &ts ) != 0 )
+ return (guint64)0;
+
+ return (guint64)timespec_load( &ts );
+}
+
+guint64 now_nsec( clockid_t clock_id )
+{
+ struct timespec ts;
+
+ if( clock_gettime( CLOCK_REALTIME, &ts ) != 0 )
+ return (guint64)0;
+
+ return timespec_load_nsec( &ts );
+}
+
+
+struct tm *localtime_or_gmtime_r( const time_t *t, struct tm *tm, gboolean utc )
+{
+ if( !t || !tm ) return NULL;
+
+ return utc ? gmtime_r(t, tm) : localtime_r(t, tm);
+}
+
+time_t mktime_or_timegm( struct tm *tm, gboolean utc )
+{
+ if( !tm ) return -1;
+
+ return utc ? timegm(tm) : mktime(tm);
+}
+
+gboolean clock_get_hwclock( struct tm *tm )
+{
+ int rc = -1;
+ gchar *ioctlname;
+
+ if( !tm )
+ return FALSE;
+
+ (void)open_rtc();
+
+ if( rtc_dev_fd < 0 )
+ {
+ g_debug( "error: Canot open '%s' device", rtc_dev_name );
+ return FALSE;
+ }
+
+ ioctlname = "RTC_RD_TIME";
+ rc = ioctl( rtc_dev_fd, RTC_RD_TIME, tm );
+ if( rc == -1 )
+ {
+ g_debug( "warning: ioctl(%s) to '%s' to read the time failed", ioctlname, rtc_dev_name );
+ }
+
+ tm->tm_isdst = -1; /* don't know whether it's dst */
+
+ close_rtc();
+
+ return TRUE;
+}
+
+gboolean clock_set_hwclock( const struct tm *tm )
+{
+ int rc = -1;
+ gchar *ioctlname;
+
+ (void)open_rtc();
+
+ if( rtc_dev_fd < 0 )
+ {
+ g_debug( "error: Canot open '%s' device", rtc_dev_name );
+ return FALSE;
+ }
+
+ ioctlname = "RTC_SET_TIME";
+ rc = ioctl( rtc_dev_fd, RTC_SET_TIME, tm );
+ if( rc == -1 )
+ {
+ g_debug( "warning: ioctl(%s) to '%s' to set the time failed", ioctlname, rtc_dev_name );
+ }
+
+ close_rtc();
+
+ return TRUE;
+}
+
+gboolean clock_set_timezone( int *ret_minutesdelta )
+{
+ struct timespec ts;
+ struct tm tm;
+ int minutesdelta;
+ struct timezone tz;
+
+ if( clock_gettime(CLOCK_REALTIME, &ts) != 0 )
+ return FALSE;
+
+ if( !localtime_r( &ts.tv_sec, &tm ) )
+ return FALSE;
+
+ minutesdelta = tm.tm_gmtoff / 60;
+
+ tz = (struct timezone)
+ {
+ .tz_minuteswest = -minutesdelta,
+ .tz_dsttime = 0, /* DST_NONE */
+ };
+
+ /* If the RTC does not run in UTC but in local time, the very first call to settimeofday() will set
+ * the kernel's timezone and will warp the system clock, so that it runs in UTC instead of the local
+ * time we have read from the RTC. */
+ if( settimeofday( NULL, &tz ) < 0 )
+ return FALSE;
+
+ if( ret_minutesdelta )
+ *ret_minutesdelta = minutesdelta;
+
+ return TRUE;
+}
+
+
+/***************************************************************
+ Timezone functions:
+ */
+gboolean timezone_is_valid( const gchar *name )
+{
+ const gchar *p, *fname;
+ int fd;
+ gchar buf[4];
+ gboolean slash = FALSE;
+ gboolean ret = TRUE;
+
+ if( !name || *name == '\0' ) return FALSE;
+
+ /*
+ Always accept "UTC" as valid timezone,
+ since it's the fallback, even if user
+ has no timezones installed.
+ */
+ if( g_strcmp0( name, "UTC" ) == 0 ) return TRUE;
+
+ if( name[0] == '/' ) return FALSE;
+ for( p = (gchar *)name; *p; ++p )
+ {
+ if( !g_ascii_isdigit( *p ) && !g_ascii_isalpha( *p ) && ( *p != '-' && *p != '_' && *p != '+' && *p != '/' ) )
+ return FALSE;
+ if( *p == '/' )
+ {
+ if( slash ) return FALSE;
+ slash = TRUE;
+ }
+ else
+ {
+ slash = FALSE;
+ }
+ }
+
+ if( slash )
+ return FALSE;
+
+ if( p - name >= PATH_MAX )
+ return FALSE; /* name too long */
+
+ fname = g_strjoin( "/", SYSTEM_ZONEINFO_DIR, name, NULL );
+
+ fd = g_open( fname, O_RDONLY | O_CLOEXEC );
+ if( fd < 0 )
+ {
+ /* log: "Failed to open timezone file '%s': %s", fname, strerror() */
+ g_free( (gpointer)fname );
+ return FALSE;
+ }
+
+ if( !g_file_test( fname, G_FILE_TEST_IS_REGULAR ) )
+ {
+ /* log: "Timezone file '%s' is not a regular file: %s", fname, strerror() */
+ g_free( (gpointer)fname );
+ return FALSE;
+ }
+
+ if( read( fd, &buf, 4 ) != 4 )
+ {
+ /* log: "Failed to read from timezone file '%s': %m", fname, strerror() */
+ g_free( (gpointer)fname );
+ return FALSE;
+ }
+
+ /* Magic from tzfile(5) */
+ if( memcmp( buf, "TZif", 4 ) != 0 )
+ {
+ /* log: "Timezone file '%s' has wrong magic bytes", fname */
+ g_free( (gpointer)fname );
+ return FALSE;
+ }
+
+ g_free( (gpointer)fname );
+
+ return ret;
+}
+
+gboolean set_system_timezone( const gchar *name )
+{
+ const gchar *fname, *source;
+ gboolean ret = TRUE;
+
+
+ if( !name || *name == '\0' || g_strcmp0( name, "UTC" ) == 0 )
+ {
+ fname = g_strjoin( "/", SYSTEM_ZONEINFO_DIR, "UTC", NULL );
+
+ if( !g_file_test( fname, G_FILE_TEST_IS_REGULAR ) )
+ {
+ /* log: "Timezone file '%s' is not a regular file: %s", fname, strerror() */
+ g_free( (gpointer)fname );
+ return FALSE;
+ }
+ source = g_strjoin( "/", "..", skip_root( SYSTEM_ZONEINFO_DIR ), "UTC", NULL );
+ }
+ else
+ {
+ source = g_strjoin( "/", "..", skip_root( SYSTEM_ZONEINFO_DIR ), name, NULL );
+ }
+
+ /* Create symlink to the new timezone */
+ unlink( "/etc/localtime" );
+ ret = symlink_atomic( (const char *)source, "/etc/localtime" );
+ g_free( (gpointer)source );
+
+ /* Make glibc notice the new timezone */
+ tzset();
+
+ /* Tell the kernel our timezone */
+ (void)clock_set_timezone( NULL );
+
+ return ret;
+}
+
+gboolean get_system_timezone( gchar **ret )
+{
+ GError *error = NULL;
+ gchar *link_target;
+ gchar *timezone;
+
+ if( !ret ) return FALSE;
+
+ link_target = g_file_read_link( "/etc/localtime", &error );
+
+ if( error != NULL )
+ {
+ timezone = g_strdup( "UTC" );
+ if( !timezone )
+ {
+ g_error_free( error );
+ g_free( (gpointer)link_target );
+ return FALSE;
+ }
+ g_error_free( error );
+ goto out;
+ }
+
+ if( !g_path_is_absolute( link_target ) )
+ {
+ gchar *absolute_link_target = g_strdup( link_target + 2 ); /* skip '..' */
+ g_free( (gpointer)link_target );
+ link_target = g_strdup( absolute_link_target );
+ g_free( (gpointer)absolute_link_target );
+ }
+
+ timezone = g_strdup( link_target + strlen( SYSTEM_ZONEINFO_DIR ) + 1 );
+
+ if( !timezone_is_valid( timezone ) )
+ {
+ g_free( (gpointer)timezone );
+ g_free( (gpointer)link_target );
+ return FALSE;
+ }
+
+out:
+ g_free( (gpointer)link_target );
+ *ret = timezone;
+
+ return TRUE;
+}
+
+/***************************************************************
+ LocalRTC functions:
+ */
+static
+gboolean exec_cmd( const gchar *cmd )
+{
+ int exit_status;
+ GError *error = NULL;
+ gboolean ret = TRUE;
+
+ if( !cmd || *cmd == '\0' ) return FALSE;
+
+ if( !g_spawn_command_line_sync( cmd, NULL, NULL, &exit_status, &error ) )
+ {
+ g_error_free( error );
+ ret = FALSE;
+ }
+ g_free( (gpointer)cmd );
+
+ if( exit_status != 0 )
+ ret = FALSE;
+
+ return ret;
+}
+
+gboolean write_data_local_rtc( gboolean local_rtc )
+{
+ gchar *w = NULL;
+ gboolean ret = TRUE;
+ gchar *cmd;
+
+ if( !g_file_test( ADJTIME_CONF, G_FILE_TEST_EXISTS ) )
+ {
+ if( !local_rtc )
+ {
+ if( !(w = g_strdup( NULL_ADJTIME_UTC )) ) return FALSE;
+ if( !(g_file_set_contents( ADJTIME_CONF, w, -1, NULL )) )
+ {
+ g_free( (gpointer)w );
+ return FALSE;
+ }
+ g_free( (gpointer)w );
+ }
+ else
+ {
+ if( !(w = g_strdup( NULL_ADJTIME_LOCAL )) ) return FALSE;
+ if( !(g_file_set_contents( ADJTIME_CONF, w, -1, NULL )) )
+ {
+ g_free( (gpointer)w );
+ return FALSE;
+ }
+ g_free( (gpointer)w );
+ }
+
+ return ret;
+ }
+
+ if( !g_file_test( HWCLOCK_CONF, G_FILE_TEST_EXISTS ) )
+ {
+ const gchar *localtime = "#\n"
+ "# /etc/hardwareclockn\n"
+ "#\n"
+ "# Tells how the hardware clock time is stored.\n"
+ "# You should run timeconfig to edit this file.\n"
+ "\n"
+ "localtime\n";
+
+ const gchar *UTC = "#\n"
+ "# /etc/hardwareclockn\n"
+ "#\n"
+ "# Tells how the hardware clock time is stored.\n"
+ "# You should run timeconfig to edit this file.\n"
+ "\n"
+ "UTC\n";
+
+ if( !local_rtc )
+ {
+ if( !(g_file_set_contents( HWCLOCK_CONF, UTC, -1, NULL )) ) return FALSE;
+ }
+ else
+ {
+ if( !(g_file_set_contents( HWCLOCK_CONF, localtime, -1, NULL )) ) return FALSE;
+ }
+
+ return ret;
+ }
+
+
+ if( !local_rtc )
+ {
+ cmd = g_strconcat( "sed -i 's,^LOCAL,UTC,' ", ADJTIME_CONF, NULL );
+ if( !exec_cmd( (const gchar *)cmd ) ) ret = FALSE;
+
+ cmd = g_strconcat( "sed -i 's,^localtime,UTC,' ", HWCLOCK_CONF, NULL );
+ if( !exec_cmd( (const gchar *)cmd ) ) ret = FALSE;
+ }
+ else
+ {
+ cmd = g_strconcat( "sed -i 's,^UTC,LOCAL,' ", ADJTIME_CONF, NULL );
+ if( !exec_cmd( (const gchar *)cmd ) ) ret = FALSE;
+
+ cmd = g_strconcat( "sed -i 's,^UTC,localtime,' ", HWCLOCK_CONF, NULL );
+ if( !exec_cmd( (const gchar *)cmd ) ) ret = FALSE;
+ }
+
+ return ret;
+}
+
+gboolean read_data_local_rtc( gboolean *local_rtc )
+{
+ gboolean ret = FALSE;
+
+ if( !local_rtc ) return FALSE;
+
+ if( g_file_test( HWCLOCK_CONF, G_FILE_TEST_EXISTS ) )
+ {
+ gchar *s = NULL;
+ gsize len;
+
+ ret = g_file_get_contents( HWCLOCK_CONF, &s, &len, NULL );
+ if( !ret )
+ return FALSE;
+
+ ret = g_regex_match_simple( "localtime", (const gchar *)s, 0, 0 );
+ if( ret )
+ {
+ *local_rtc = TRUE;
+ g_free( (gpointer)s );
+ return TRUE;
+ }
+
+ ret = g_regex_match_simple ( "UTC", (const gchar *)s, 0, 0 );
+ if( ret )
+ {
+ *local_rtc = FALSE;
+ g_free( (gpointer)s );
+ return TRUE;
+ }
+
+ return FALSE;
+ }
+ else if( g_file_test( ADJTIME_CONF, G_FILE_TEST_EXISTS ) )
+ {
+ gchar *s = NULL;
+ gsize len;
+
+ ret = g_file_get_contents( ADJTIME_CONF, &s, &len, NULL );
+ if( !ret )
+ return FALSE;
+
+ ret = g_regex_match_simple( "LOCAL", (const gchar *)s, 0, 0 );
+ if( ret )
+ {
+ *local_rtc = TRUE;
+ g_free( (gpointer)s );
+ return TRUE;
+ }
+
+ ret = g_regex_match_simple ( "UTC", (const gchar *)s, 0, 0 );
+ if( ret )
+ {
+ *local_rtc = FALSE;
+ g_free( (gpointer)s );
+ return TRUE;
+ }
+
+ return FALSE;
+ }
+ else
+ {
+ return ret;
+ }
+
+ return ret;
+}