freebsd-skq/sbin/adjkerntz/adjkerntz.c
Guy Helmer d5ac36eda7 Invoke tzset(3) within the main loop to catch changes to /etc/localtime
when running as a daemon in the hope that it will fix situations
where the CMOS clock was apparently set using the stale TZ offset.
2004-04-27 14:43:03 +00:00

387 lines
9.5 KiB
C

/*
* Copyright (C) 1993-1998 by Andrey A. Chernov, Moscow, Russia.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
#if 0
#ifndef lint
static const char copyright[] =
"@(#)Copyright (C) 1993-1996 by Andrey A. Chernov, Moscow, Russia.\n\
All rights reserved.\n";
#endif /* not lint */
#endif
#include <sys/cdefs.h>
__FBSDID("$FreeBSD$");
/*
* Andrey A. Chernov <ache@astral.msk.su> Dec 20 1993
*
* Fix kernel time value if machine run wall CMOS clock
* (and /etc/wall_cmos_clock file present)
* using zoneinfo rules or direct TZ environment variable set.
* Use Joerg Wunsch idea for seconds accurate offset calculation
* with Garrett Wollman and Bruce Evans fixes.
*
*/
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include <syslog.h>
#include <sys/time.h>
#include <sys/param.h>
#include <machine/cpu.h>
#include <sys/sysctl.h>
#include "pathnames.h"
/*#define DEBUG*/
#define True (1)
#define False (0)
#define Unknown (-1)
#define REPORT_PERIOD (30*60)
static void fake(int);
static void usage(void);
static void
fake(int unused __unused)
{
/* Do nothing. */
}
int
main(int argc, char *argv[])
{
struct tm local;
struct timeval tv, *stv;
struct timezone tz, *stz;
int kern_offset, wall_clock, disrtcset;
size_t len;
int mib[2];
/* Avoid time_t here, can be unsigned long or worse */
long offset, localsec, diff;
time_t initial_sec, final_sec;
int ch;
int initial_isdst = -1, final_isdst;
int need_restore = False, sleep_mode = False, looping,
init = Unknown;
sigset_t mask, emask;
while ((ch = getopt(argc, argv, "ais")) != -1)
switch((char)ch) {
case 'i': /* initial call, save offset */
if (init != Unknown)
usage();
init = True;
break;
case 'a': /* adjustment call, use saved offset */
if (init != Unknown)
usage();
init = False;
break;
case 's':
sleep_mode = True;
break;
default:
usage();
}
if (init == Unknown)
usage();
if (access(_PATH_CLOCK, F_OK) != 0)
return 0;
if (init)
sleep_mode = True;
sigemptyset(&mask);
sigemptyset(&emask);
sigaddset(&mask, SIGTERM);
openlog("adjkerntz", LOG_PID|LOG_PERROR, LOG_DAEMON);
(void) signal(SIGHUP, SIG_IGN);
if (init && daemon(0, 1)) {
syslog(LOG_ERR, "daemon: %m");
return 1;
}
again:
(void) sigprocmask(SIG_BLOCK, &mask, NULL);
(void) signal(SIGTERM, fake);
diff = 0;
stv = NULL;
stz = NULL;
looping = False;
wall_clock = (access(_PATH_CLOCK, F_OK) == 0);
if (init && !sleep_mode) {
init = False;
if (!wall_clock)
return 0;
}
tzset();
mib[0] = CTL_MACHDEP;
mib[1] = CPU_ADJKERNTZ;
len = sizeof(kern_offset);
if (sysctl(mib, 2, &kern_offset, &len, NULL, 0) == -1) {
syslog(LOG_ERR, "sysctl(get_offset): %m");
return 1;
}
/****** Critical section, do all things as fast as possible ******/
/* get local CMOS clock and possible kernel offset */
if (gettimeofday(&tv, &tz)) {
syslog(LOG_ERR, "gettimeofday: %m");
return 1;
}
/* get the actual local timezone difference */
initial_sec = tv.tv_sec;
recalculate:
local = *localtime(&initial_sec);
if (diff == 0)
initial_isdst = local.tm_isdst;
local.tm_isdst = initial_isdst;
/* calculate local CMOS diff from GMT */
localsec = mktime(&local);
if (localsec == -1) {
/*
* XXX user can only control local time, and it is
* unacceptable to fail here for init. 2:30 am in the
* middle of the nonexistent hour means 3:30 am.
*/
if (!sleep_mode) {
syslog(LOG_WARNING,
"Warning: nonexistent local time, try to run later.");
syslog(LOG_WARNING, "Giving up.");
return 1;
}
syslog(LOG_WARNING,
"Warning: nonexistent local time.");
syslog(LOG_WARNING, "Will retry after %d minutes.",
REPORT_PERIOD / 60);
(void) signal(SIGTERM, SIG_DFL);
(void) sigprocmask(SIG_UNBLOCK, &mask, NULL);
(void) sleep(REPORT_PERIOD);
goto again;
}
offset = -local.tm_gmtoff;
#ifdef DEBUG
fprintf(stderr, "Initial offset: %ld secs\n", offset);
#endif
/* correct the kerneltime for this diffs */
/* subtract kernel offset, if present, old offset too */
diff = offset - tz.tz_minuteswest * 60 - kern_offset;
if (diff != 0) {
#ifdef DEBUG
fprintf(stderr, "Initial diff: %ld secs\n", diff);
#endif
/* Yet one step for final time */
final_sec = initial_sec + diff;
/* get the actual local timezone difference */
local = *localtime(&final_sec);
final_isdst = diff < 0 ? initial_isdst : local.tm_isdst;
if (diff > 0 && initial_isdst != final_isdst) {
if (looping)
goto bad_final;
looping = True;
initial_isdst = final_isdst;
goto recalculate;
}
local.tm_isdst = final_isdst;
localsec = mktime(&local);
if (localsec == -1) {
bad_final:
/*
* XXX as above. The user has even less control,
* but perhaps we never get here.
*/
if (!sleep_mode) {
syslog(LOG_WARNING,
"Warning: nonexistent final local time, try to run later.");
syslog(LOG_WARNING, "Giving up.");
return 1;
}
syslog(LOG_WARNING,
"Warning: nonexistent final local time.");
syslog(LOG_WARNING, "Will retry after %d minutes.",
REPORT_PERIOD / 60);
(void) signal(SIGTERM, SIG_DFL);
(void) sigprocmask(SIG_UNBLOCK, &mask, NULL);
(void) sleep(REPORT_PERIOD);
goto again;
}
offset = -local.tm_gmtoff;
#ifdef DEBUG
fprintf(stderr, "Final offset: %ld secs\n", offset);
#endif
/* correct the kerneltime for this diffs */
/* subtract kernel offset, if present, old offset too */
diff = offset - tz.tz_minuteswest * 60 - kern_offset;
if (diff != 0) {
#ifdef DEBUG
fprintf(stderr, "Final diff: %ld secs\n", diff);
#endif
/*
* stv is abused as a flag. The important value
* is in `diff'.
*/
stv = &tv;
}
}
if (tz.tz_dsttime != 0 || tz.tz_minuteswest != 0) {
tz.tz_dsttime = tz.tz_minuteswest = 0; /* zone info is garbage */
stz = &tz;
}
if (!wall_clock && stz == NULL)
stv = NULL;
/* if init or UTC clock and offset/date will be changed, */
/* disable RTC modification for a while. */
if ( (init && stv != NULL)
|| ((init || !wall_clock) && kern_offset != offset)
) {
mib[0] = CTL_MACHDEP;
mib[1] = CPU_DISRTCSET;
len = sizeof(disrtcset);
if (sysctl(mib, 2, &disrtcset, &len, NULL, 0) == -1) {
syslog(LOG_ERR, "sysctl(get_disrtcset): %m");
return 1;
}
if (disrtcset == 0) {
disrtcset = 1;
need_restore = True;
if (sysctl(mib, 2, NULL, NULL, &disrtcset, len) == -1) {
syslog(LOG_ERR, "sysctl(set_disrtcset): %m");
return 1;
}
}
}
if ( (init && (stv != NULL || stz != NULL))
|| (stz != NULL && stv == NULL)
) {
if (stv != NULL) {
/*
* Get the time again, as close as possible to
* adjusting it, to minimise drift.
* XXX we'd better not fail between here and
* restoring disrtcset, since we don't clean up
* anything.
*/
if (gettimeofday(&tv, (struct timezone *)NULL)) {
syslog(LOG_ERR, "gettimeofday: %m");
return 1;
}
tv.tv_sec += diff;
stv = &tv;
}
if (settimeofday(stv, stz)) {
syslog(LOG_ERR, "settimeofday: %m");
return 1;
}
}
/* setting CPU_ADJKERNTZ have a side effect: resettodr(), which */
/* can be disabled by CPU_DISRTCSET, so if init or UTC clock */
/* -- don't write RTC, else write RTC. */
if (kern_offset != offset) {
kern_offset = offset;
mib[0] = CTL_MACHDEP;
mib[1] = CPU_ADJKERNTZ;
len = sizeof(kern_offset);
if (sysctl(mib, 2, NULL, NULL, &kern_offset, len) == -1) {
syslog(LOG_ERR, "sysctl(update_offset): %m");
return 1;
}
}
mib[0] = CTL_MACHDEP;
mib[1] = CPU_WALLCLOCK;
len = sizeof(wall_clock);
if (sysctl(mib, 2, NULL, NULL, &wall_clock, len) == -1) {
syslog(LOG_ERR, "sysctl(put_wallclock): %m");
return 1;
}
if (need_restore) {
need_restore = False;
mib[0] = CTL_MACHDEP;
mib[1] = CPU_DISRTCSET;
disrtcset = 0;
len = sizeof(disrtcset);
if (sysctl(mib, 2, NULL, NULL, &disrtcset, len) == -1) {
syslog(LOG_ERR, "sysctl(restore_disrtcset): %m");
return 1;
}
}
/****** End of critical section ******/
if (init && wall_clock) {
sleep_mode = False;
/* wait for signals and acts like -a */
(void) sigsuspend(&emask);
goto again;
}
return 0;
}
static void
usage(void)
{
fprintf(stderr, "%s\n%s\n%s\n%s\n",
"usage: adjkerntz -i",
"\t\t(initial call from /etc/rc)",
" adjkerntz -a [-s]",
"\t\t(adjustment call, -s for sleep/retry mode)");
exit(2);
}