/* V1.0

 * CMOS clock manipulation - Charles Hedrick, hedrick@cs.rutgers.edu, Apr 1992
 * 
 * clock [-u] -r  - read cmos clock
 * clock [-u] -w  - write cmos clock from system time
 * clock [-u] -s  - set system time from cmos clock
 * clock [-u] -a  - set system time from cmos clock, adjust the time to
 *                  correct for systematic error, and put it back to the cmos.
 *
 *  -u indicates cmos clock is kept in universal time
 *
 * THIS HAS NOT BEEN INCORPORATED:
 *  -A indicates cmos clock is kept in Alpha ARC console time (0 == 1980)
 *  -J indicates we're dealing with a Jensen (early DEC Alpha PC)
 *
 * The program is designed to run setuid, since we need to be able to
 * write the CMOS port.
 *
 * I don't know what the CMOS clock will do in 2000, so this program
 * probably won't work past the century boundary.
 *
 *********************
 * V1.1
 * Modified for clock adjustments - Rob Hooft, hooft@chem.ruu.nl, Nov 1992
 * Also moved error messages to stderr. The program now uses getopt.
 * Changed some exit codes. Made 'gcc 2.3 -Wall' happy.
 *
 * I think a small explanation of the adjustment routine should be given
 * here. The problem with my machine is that its CMOS clock is 10 seconds 
 * per day slow. With this version of clock.c, and my '/etc/rc.local' 
 * reading '/etc/clock -au' instead of '/etc/clock -u -s', this error 
 * is automatically corrected at every boot. 
 *
 * To do this job, the program reads and writes the file '/etc/adjtime' 
 * to determine the correction, and to save its data. In this file are 
 * three numbers: 
 *
 * 1) the correction in seconds per day (So if your clock runs 5 
 *    seconds per day fast, the first number should read -5.0)
 * 2) the number of seconds since 1/1/1970 the last time the program was
 *    used.
 * 3) the remaining part of a second which was leftover after the last 
 *    adjustment
 *
 * Installation and use of this program:
 *
 * a) create a file '/etc/adjtime' containing as the first and only line:
 *    '0.0 0 0.0'
 * b) run 'clock -au' or 'clock -a', depending on whether your cmos is in
 *    universal or local time. This updates the second number.
 * c) set your system time using the 'date' command.
 * d) update your cmos time using 'clock -wu' or 'clock -w'
 * e) replace the first number in /etc/adjtime by your correction.
 * f) put the command 'clock -au' or 'clock -a' in your '/etc/rc.local' or
 *    let cron(8) run it regularly.
 *
 * If the adjustment doesn't work for you, try contacting me by E-mail.
 *
 ******
 * V1.2
 *
 * Applied patches by Harald Koenig (koenig@nova.tat.physik.uni-tuebingen.de)
 * Patched and indented by Rob Hooft (hooft@EMBL-Heidelberg.DE)
 * 
 * A free quote from a MAIL-message (with spelling corrections):
 *
 * "I found the explanation and solution for the CMOS reading 0xff problem
 *  in the 0.99pl13c (ALPHA) kernel: the RTC goes offline for a small amount
 *  of time for updating. Solution is included in the kernel source 
 *  (linux/kernel/time.c)."
 *
 * "I modified clock.c to fix this problem and added an option (now default,
 *  look for USE_INLINE_ASM_IO) that I/O instructions are used as inline
 *  code and not via /dev/port (still possible via #undef ...)."
 *
 * With the new code, which is partially taken from the kernel sources, 
 * the CMOS clock handling looks much more "official".
 * Thanks Harald (and Torsten for the kernel code)!
 *
 ******
 * V1.3
 * Canges from alan@spri.levels.unisa.edu.au (Alan Modra):
 * a) Fix a few typos in comments and remove reference to making
 *    clock -u a cron job.  The kernel adjusts cmos time every 11
 *    minutes - see kernel/sched.c and kernel/time.c set_rtc_mmss().
 *    This means we should really have a cron job updating
 *    /etc/adjtime every 11 mins (set last_time to the current time
 *    and not_adjusted to ???).
 * b) Swapped arguments of outb() to agree with asm/io.h macro of the
 *    same name.  Use outb() from asm/io.h as it's slightly better.
 * c) Changed CMOS_READ and CMOS_WRITE to inline functions.  Inserted
 *    cli()..sti() pairs in appropriate places to prevent possible
 *    errors, and changed ioperm() call to iopl() to allow cli.
 * d) Moved some variables around to localise them a bit.
 * e) Fixed bug with clock -ua or clock -us that cleared environment
 *    variable TZ.  This fix also cured the annoying display of bogus
 *    day of week on a number of machines. (Use mktime(), ctime()
 *    rather than asctime() )
 * f) Use settimeofday() rather than stime().  This one is important
 *    as it sets the kernel's timezone offset, which is returned by
 *    gettimeofday(), and used for display of MSDOS and OS2 file
 *    times.
 * g) faith@cs.unc.edu added -D flag for debugging
 *
 ******
 * V1.4: alan@SPRI.Levels.UniSA.Edu.Au (Alan Modra)
 *       Wed Feb  8 12:29:08 1995, fix for years > 2000.
 *       faith@cs.unc.edu added -v option to print version.
 *
 *******
 * V1.5: Paul Gortmaker -- 04/02/96
 *
 *	Use ioctl() on /dev/rtc if we have it. Fall back to directly
 * 	accessing the RTC otherwise. Linux-SMP users will have to use the
 *	/dev/rtc method, as cli/sti from user space is not SMP friendly.
 *
 ******
 * V1.6  janl@math.uio.no (Nicolai Langfeldt):
 *       What I REALLY want is a /dev/rtc driver for all architectures...
 *
 *       - Added errorchecking on mktime returnvalues, based on patch submitted
 *	   by dan@gasboy.com (Dan Wilder).
 *       - Fred C. Smith (fredex@fcshome.stoneham.ma.us):
 *         Fixed error in code for -a option that scaled the factor by the
 *         elapsed time since last adjustment. It was not adding in the
 *         leftover amount from the last adjustment.
 *         Also changed so it won't update /etc/adjtime if adjustment == 0
 *         because the cmos clock really wasn't adjusted!
 *       - Incorporated linux/68k patches by Roman Hodek
 *	   (Roman.Hodek@informatik.uni-erlangen.de):
 * 	   Reading/writing the hardware clock registers directly or via
 *         /dev/port is not the English way... Such hardware dependant stuff
 *         can never work on other architectures.
 *         Linux/68k has new ioctls providing an interface for reading and
 *         writing the RTC. Now (if KDGHWCLK is defined), clock first tries to
 *         use the ioctl, and only if that fails, it writes to the I/O ports
 *         the old way for kernels that don't know about KDGHWCLK. Maybe the
 *         ioctls should be implemented in all Linuxes in future...
 */

#define VERSION "1.6"

#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <getopt.h>
#include <time.h>
#include <sys/time.h>
#include <string.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/ioctl.h>

#include <linux/version.h>
/* Check if the /dev/rtc interface is available in this version of
   the system headers.  131072 is linux 2.0.0.  Might need to make
   it condtional on i386 or something too -janl */
#if LINUX_VERSION_CODE >= 131072
#  define DEV_RTC_AVAILABLE 1
#  include <linux/mc146818rtc.h>
#  include <linux/kd.h>
#endif

#ifdef __i386__
#define USE_INLINE_ASM_IO
#endif

#ifdef USE_INLINE_ASM_IO
#include <asm/io.h>
#endif

/* Here the information for time adjustments is kept. */
#define ADJPATH "/etc/adjtime"


/* used for debugging the code, keeps it off fiddeling with the RTC */
#undef KEEP_OFF

/* Globals */
int readit = 0;
int adjustit = 0;
int writeit = 0;
int setit = 0;
int universal = 0;
int debug = 0;
int have_dev_rtc = 0;
int cmos_fd = -1;

volatile void 
usage ()
{
  fprintf (stderr, 
    "usage: clock [-u] -r|w|s|a|v|J|A\n"
    "  r: read and print CMOS clock\n"
    "  w: write CMOS clock from system time\n"
    "  s: set system time from CMOS clock\n"
    "  a: get system time and adjust CMOS clock\n"
    "  u: CMOS clock is in universal time\n"
	   /*    "  J: RTC is Alpha/Jensen design\n" */
	   /*   "  A: RTC is Alpha/ARC design\n" */
    "  v: print version (" VERSION ") and exit\n"
  );
  exit (1);
}

#ifdef __i386__

/*********************** Intel ************************/

static inline unsigned char cmos_read(unsigned char reg)
{
  register unsigned char ret;
  __asm__ volatile ("cli");
  outb (reg | 0x80, 0x70);
  ret = inb (0x71);
  __asm__ volatile ("sti");
  return ret;
}


static inline void cmos_write(unsigned char reg, unsigned char val)
{
  outb (reg | 0x80, 0x70);
  outb (val, 0x71);
}

# ifndef outb
static inline void 
outb (char val, unsigned short port)
{
#  ifdef USE_INLINE_ASM_IO
     __asm__ volatile ("out%B0 %0,%1"::"a" (val), "d" (port));
#  else
     lseek (cmos_fd, port, 0);
     write (cmos_fd, &val, 1);
#  endif
}
# endif

# ifndef inb
static inline unsigned char 
inb (unsigned short port)
{
  unsigned char ret;
#  ifdef USE_INLINE_ASM_IO
     __asm__ volatile ("in%B0 %1,%0":"=a" (ret):"d" (port));
#  else
     lseek (cmos_fd, port, 0);
     read (cmos_fd, &ret, 1);
#  endif
  return ret;
}
# endif

#elif defined(__mc68000__)
/*********************** m68k: dummies ************************/

static inline unsigned char cmos_read(unsigned char reg)
{
}

static inline void cmos_write(unsigned char reg, unsigned char val)
{
}

#endif

void 
cmos_init ()
{

#ifdef DEV_RTC_AVAILABLE
  /*
   * Test if we can use the safer "/dev/rtc"
   */
  cmos_fd = open("/dev/rtc",O_RDONLY);
  if (cmos_fd != -1) {
    have_dev_rtc = 1;
    if (debug)
      printf("Using ioctl() on \"/dev/rtc\" instead of iopl(3)\n");
    return;
  }
  if (debug) {
     perror("/dev/rtc");
     printf( "Using iopl(3) fallback\n" );
  }
#endif

  /*
   * No /dev/rtc to ioctl() -- have to use direct access. NB: this
   * breaks on SMP machines.
   */

#ifdef USE_INLINE_ASM_IO
  if (iopl (3))
    {
      perror("clock: unable to get I/O port access");
      exit (1);
    }
#elif !defined(KDGHWCLK)
  if (cmos_fd != -1) return;
  cmos_fd = open ("/dev/port", 2);
  if (cmos_fd < 0)
    {
      perror ("unable to open /dev/port read/write : ");
      exit (1);
    }
  if (lseek (cmos_fd, 0x70, 0) < 0 || lseek (cmos_fd, 0x71, 0) < 0)
    {
      perror ("unable to seek port 0x70 in /dev/port : ");
      exit (1);
    }
#endif
}

static inline int 
cmos_read_bcd (int addr)
{
  int b;
  b = cmos_read (addr);
  return (b & 15) + (b >> 4) * 10;
}

static inline void 
cmos_write_bcd (int addr, int value)
{
  cmos_write (addr, ((value / 10) << 4) + value % 10);
}


static void read_time(struct tm *tm)
{
  int i;
  int retval;
  unsigned long data;

#ifdef KDGHWCLK
  /* Use the m68k ioctl interface */

  int con_fd;
  struct hwclk_time t;
  
  if ((con_fd = open( "/dev/console", O_RDONLY )) < 0) {
    perror( "open(/dev/console)" );
    cmos_init();
    goto intel_read;
  }
  if (ioctl( con_fd, KDGHWCLK, &t ) < 0) {
    if (errno == EINVAL) {
      /* KDGHWCLK not implemented in this kernel... */
      cmos_init();
      goto intel_read;
    }
    else {
      perror( "ioctl(KDGHWCLK)" );
      exit( 1 );
    }
  }
  close( con_fd );
  tm->tm_sec  = t.sec;
  tm->tm_min  = t.min;
  tm->tm_hour = t.hour;
  tm->tm_mday = t.day;
  tm->tm_mon  = t.mon;
  tm->tm_year = t.year;
  tm->tm_wday = t.wday;
  tm->tm_isdst = -1;
  return;
       
intel_read:

#endif /* KDGHWCLK */

#ifdef DEV_RTC_AVAILABLE
  if (have_dev_rtc) {
    /* Use the /dev/rtc ioctl interface */

    /* This is much nicer than the non /dev/rtc ugliness
       below. We just enable update complete interrupts, and wait
       for one. That gives us the same result as busy polling on
       the RTC update bit. */

    if (debug) printf( "Turn update interrupts on\n" );
    /* Turn on update interrupts (one per second) */
    retval = ioctl(cmos_fd, RTC_UIE_ON, 0);
    if (retval == -1) {
      perror("ioctl");
      exit(errno);
    }

    /* this blocks */
    retval = read(cmos_fd, &data, sizeof(unsigned long));
    if (retval == -1) {
      perror("read");
      exit(errno);
    }

    /* Read the RTC time/date */
    retval = ioctl(cmos_fd, RTC_RD_TIME, tm);
    if (retval == -1) {
      perror("ioctl");
      exit(errno);
    }

    /* Turn off update interrupts */
    retval = ioctl(cmos_fd, RTC_UIE_OFF, 0);
    if (retval == -1) {
      perror("ioctl");
      exit(errno);
    }
	 
    if (debug) {
      printf( "Update interrupts off, tm = %p\n", tm );
      printf ("Cmos time via RTC_RD_TIME: %02d:%02d:%02d\n",
	      tm->tm_hour, tm->tm_min, tm->tm_sec);
    }

    tm->tm_isdst = -1;		/* don't know whether it's daylight */

    return;
  } /* have_dev_rtc */
  if (debug) printf( "Cmos time via RTC_RD_TIME failed, using fallback\n" );

#endif /* DEV_RTC_AVAILABLE */

  /* Fall back to dumb cmos manipulations */
  
  /* read RTC exactly on falling edge of update flag */
  /* Wait for rise.... (may take upto 1 second) */

  for (i = 0; i < 10000000; i++)	
    if (cmos_read (10) & 0x80)
      break;

  /* Wait for fall.... (must try at least 2.228 ms) */
  
  for (i = 0; i < 1000000; i++)	
    if (!(cmos_read (10) & 0x80))
      break;

  /* The purpose of the "do" loop is called "low-risk programming" */
  /* In theory it should never run more than once */
  do
    { 
      tm->tm_sec = cmos_read_bcd (0);
      tm->tm_min = cmos_read_bcd (2);
      tm->tm_hour = cmos_read_bcd (4);
      tm->tm_wday = cmos_read_bcd (6);
      tm->tm_mday = cmos_read_bcd (7);
      tm->tm_mon = cmos_read_bcd (8);
      tm->tm_year = cmos_read_bcd (9);
    }
  while (tm->tm_sec != cmos_read_bcd (0));
  if (tm->tm_year < 70)
    tm->tm_year += 100; /* 70..99 => 1970..1999, 0..69 => 2000..2069 */
  tm->tm_mon--;		/* DOS uses 1 base */
  tm->tm_wday -= 3;	/* DOS uses 3 - 9 for week days */
  tm->tm_isdst = -1;	/* don't know whether it's daylight */
}


static void write_time( struct tm *tm )

{      
  unsigned char   save_control, save_freq_select;

#ifdef KDGHWCLK

  int con_fd;
  struct hwclk_time t;

  if ((con_fd = open( "/dev/console", O_RDONLY )) < 0) {
    perror( "open(/dev/console)" );
    cmos_init();
    goto intel_write;
  }
  t.sec  = tm->tm_sec;
  t.min  = tm->tm_min;
  t.hour = tm->tm_hour;
  t.day  = tm->tm_mday;
  t.mon  = tm->tm_mon;
  t.year = tm->tm_year;
  t.wday = tm->tm_wday;
  if (ioctl( con_fd, KDSHWCLK, &t ) < 0) {
    if (errno == EINVAL) {
      /* KDSHWCLK not implemented in this kernel... */
      cmos_init();
      goto intel_write;
    } else {
      perror( "ioctl(KDSHWCLK)" );
      exit( 1 );
    }
  }
  close( con_fd );
  return;

intel_write:

#endif /* GDGHWCLK */

#ifdef DEV_RTC_AVAILABLE
  if (have_dev_rtc) {

    int retval;

    retval = ioctl(cmos_fd, RTC_SET_TIME, tm);
    if (retval == -1) {
      perror("ioctl");
      exit(errno);
    }

    return;
  }
#endif  /* DEV_RTC_AVAILABLE */

#ifdef __i386__
  __asm__ volatile ("cli");
#endif
  save_control = cmos_read (11);   /* tell the clock it's being set */
  cmos_write (11, (save_control | 0x80));
  save_freq_select = cmos_read (10);       /* stop and reset prescaler */
  cmos_write (10, (save_freq_select | 0x70));

  cmos_write_bcd (0, tm->tm_sec);
  cmos_write_bcd (2, tm->tm_min);
  cmos_write_bcd (4, tm->tm_hour);
  cmos_write_bcd (6, tm->tm_wday + 3);
  cmos_write_bcd (7, tm->tm_mday);
  cmos_write_bcd (8, tm->tm_mon + 1);
  cmos_write_bcd (9, tm->tm_year);

  /* The kernel sources, linux/arch/i386/kernel/time.c, have the
   * following comment:
   *
   *    The following flags have to be released exactly in this order,
   *    otherwise the DS12887 (popular MC146818A clone with integrated
   *    battery and quartz) will not reset the oscillator and will not
   *    update precisely 500 ms later.  You won't find this mentioned
   *    in the Dallas Semiconductor data sheets, but who believes data
   *    sheets anyway ...  -- Markus Kuhn
   *
   * Hence, they will also be done in this order here.
   * faith@cs.unc.edu, Thu Nov  9 08:26:37 1995 */
      
  cmos_write (11, save_control);
  cmos_write (10, save_freq_select);
#ifdef __i386__
  __asm__ volatile ("sti");
#endif
}


int 
main (int argc, char **argv, char **envp)
{
  struct tm tm;
  time_t systime;
  time_t last_time;
  char arg;
  double factor;
  double not_adjusted;
  int adjustment = 0;

  while ((arg = getopt (argc, argv, "rwsuaDv")) != -1)
    {
      switch (arg)
	{
	case 'r':
	  readit = 1;
	  break;
	case 'w':
	  writeit = 1;
	  break;
	case 's':
	  setit = 1;
	  break;
	case 'u':
	  universal = 1;
	  break;
	case 'a':
	  adjustit = 1;
	  break;
        case 'D':
	  debug = 1;
	  break;
	case 'v':
	  fprintf( stderr, "util-linux clock " VERSION "\n" );
	  exit(0);
	default:
	  usage ();
	}
    }

  if (readit + writeit + setit + adjustit > 1)
    usage ();			/* only allow one of these */

  if (!(readit | writeit | setit | adjustit))	/* default to read */
    readit = 1;

  cmos_init ();

  if (adjustit)
    {				/* Read adjustment parameters first */
      FILE *adj;
      if ((adj = fopen (ADJPATH, "r")) == NULL)
	{
	  perror (ADJPATH);
	  exit (2);
	}
      if (fscanf (adj, "%lf %lu %lf", &factor, &last_time, &not_adjusted) < 0)
	{
	  perror (ADJPATH);
	  exit (2);
	}
      fclose (adj);
      if (debug) 
	printf ("Last adjustment done at %lu seconds after 1/1/1970\n", 
		(unsigned long)last_time);
    }

  if (readit || setit || adjustit)
    {
#ifndef KEEP_OFF
      read_time( &tm );
#endif
      if (debug) 
	printf ("Cmos time : %2d:%02d:%02d %d/%d/%d %d\n", tm.tm_hour,
		tm.tm_min, tm.tm_sec, tm.tm_mon, tm.tm_mday, tm.tm_year,
		tm.tm_wday );
    }

  if (readit || setit || adjustit)
    {
/*
 * mktime() assumes we're giving it local time.  If the CMOS clock
 * is in GMT, we have to set up TZ so mktime knows it.  tzset() gets
 * called implicitly by the time code, but only the first time.  When
 * changing the environment variable, better call tzset() explicitly.
 */
      if (universal)
	{
	  char *zone;
	  zone = (char *) getenv ("TZ");	/* save original time zone */
	  (void) putenv ("TZ=");
	  tzset ();
	  systime = mktime (&tm);
	  if (systime == -1) {
	    fprintf (stderr, "Error in mktime: returned -1, aborting\n");
	    exit(2);
	  }
	  /* now put back the original zone */
	  if (zone)
	    {

             char *zonebuf;
             zonebuf = malloc (strlen (zone) + 4);
             strcpy (zonebuf, "TZ=");
             strcpy (zonebuf+3, zone);
             putenv (zonebuf);
             free (zonebuf);
	    }
	  else
	    {			/* wasn't one, so clear it */
	      putenv ("TZ");
	    }
	  tzset ();
	}
      else
	{
	  systime = mktime (&tm);
	  if (systime == -1) {
	    fprintf (stderr, "Error in mktime: returned -1, aborting\n");
	    exit(2);
	  }
	}
      if (debug) printf ("Number of seconds since 1/1/1970 is %lu\n",
			 (unsigned long)systime);
    }

  if (readit)
    {
      printf ("%s", ctime (&systime ));
    }

  if (setit || adjustit)
    {
      struct timeval tv;
      struct timezone tz;

/* program is designed to run setuid, be secure! */

      if (getuid () != 0)
	{			
	  fprintf (stderr, "Sorry, must be root to set or adjust time\n");
	  exit (2);
	}

      if (adjustit)
	{			/* the actual adjustment */
	  double exact_adjustment;

	  exact_adjustment = ((double) (systime - last_time))
	    * factor / (24 * 60 * 60) + not_adjusted;
	  if (exact_adjustment > 0)
	    adjustment = (int) (exact_adjustment + 0.5);
	  else
	    adjustment = (int) (exact_adjustment - 0.5);
	  not_adjusted = exact_adjustment - (double) adjustment;
	  systime += adjustment;
	  if (debug) {
	     printf ("Time since last adjustment is %d seconds\n",
		     (int) (systime - last_time));
	     printf ("Adjusting time by %d seconds\n",
		     adjustment);
	     printf ("remaining adjustment is %.3f seconds\n",
		     not_adjusted);
	  }
	}
#ifndef KEEP_OFF
      tv.tv_sec = systime;
      tv.tv_usec = 0;
      tz.tz_minuteswest = timezone / 60;
      tz.tz_dsttime = daylight;

      if (settimeofday (&tv, &tz) != 0)
        {
	  fprintf (stderr,
		   "Unable to set time -- probably you are not root\n");
	  exit (1);
	}
      
      if (debug) {
	 printf( "Called settimeofday:\n" );
	 printf( "\ttv.tv_sec = %lu, tv.tv_usec = %lu\n",
		 (unsigned long)tv.tv_sec, (unsigned long)tv.tv_usec );
	 printf( "\ttz.tz_minuteswest = %d, tz.tz_dsttime = %d\n",
		 tz.tz_minuteswest, tz.tz_dsttime );
      }
#endif
    }
  
  if (writeit || (adjustit && adjustment != 0))
    {
      struct tm *tmp;
      systime = time (NULL);
      if (universal)
	tmp = gmtime (&systime);
      else
	tmp = localtime (&systime);

#ifndef KEEP_OFF
      write_time(tmp);
#endif
      if (debug) 
	printf ("Set to : %2d:%02d:%02d\n", tmp->tm_hour, tmp->tm_min, tmp->tm_sec);
    }
  else
    if (debug) printf ("CMOS clock unchanged.\n");
  /* Save data for next 'adjustit' call */
  if (adjustit && (adjustment != 0 ||
		   (adjustment == 0 && factor == 0.0 && not_adjusted == 0.0)))
    {
      FILE *adj;
      if ((adj = fopen (ADJPATH, "w")) == NULL)
	{
	  perror (ADJPATH);
	  exit (2);
	}
      fprintf (adj, "%f %lu %f\n",
	       factor, (unsigned long)systime, not_adjusted);
      fclose (adj);
    }
  exit (0);
}
