Work: Let's Synchronize Our Watches

Jeffreys Copeland & Haemer

(RS/magazine, January 1997)


10 ... 9 ... 8 ... 7 ... 6 ... 5 ... 4 ... 3 ... 2 ... 1 ... Happy New Year!

How did you know it was midnight? Really midnight? Nerds like us might have called 303-499-7111, and spent the evening listening to the National Institute of Standards and Technology (nee, National Bureau of Standards) time signal as broadcast on WWV. (Cinematic aside: which movie is your New Year's Eve favorite? The Jeffreys are agreed on ``It's a Wonderful Life'' as the Christmas movie of choice, but have been debating the merits of ``The Fabulous Baker Boys'' versus ``The Apartment'' for New Year's.)

But we digress: There's an easier way to find out the time than sitting with the telephone pressed to your ear all night, and that's to resync your computer's clock directly to someone who knows the right time, like NIST.



Your friend, telnet.

The Internet utility telnet dates back more than twenty years. Classically, it's used to connect to another computer remotely. Since it doesn't require tight communications on both ends, it's better over a distance than rlogin.

However, logging into a remote computer isn't the only trick that telnet knows. If you take a look at /etc/services, you'll find a list of functions that can be accomplished across a TCP or UDP link. One of telnet's tricks is to allow you access to any of those functions.

For our first experiment, pick a machine near you -- for argument's sake, we'll dub it barney-- and utter

telnet barney echo
to your computer. You'll find yourself connected to barney, and have it echoing back everything you type.

Next try:

telnet barney daytime
You'll be rewarded with barney's notion of the correct date and time. Quick, type
date
and see if your clock agrees with barney's. It almost certainly doesn't.

How to fix that?

(Okay, let's head off a bunch of mail by admitting that we know there are standard utilities for solving this. We use rdate on our Sun desktop machines, for example. Ultimately, the machines in our local cluster all synchronize their clocks to QMS's firewall, which synchronizes to pogo.udel.edu, which, in turn, relys on an atomic clock for its time.)



IPC with Perl.
Perl 5 provides a number of features for connecting to a remote computer. We're going to use a variety of them. Basically, we'll establish a TCP connection with the remote machine, set up a socket to communicate with it, read the time from the remote computer, and set our time to be the same.

Let's begin with some setup:

require 5.002;
use strict;
use POSIX;
# debug flag
my $d = $main::d ? $main::d : 0;

This ensures (in order) that we're executing at least version 5.002 of Perl, that we strictly enforce safety, we include the POSIX.1 interfaces, and that we have an alias for the debug flag. (That last is necessary because we are using strict: we must fully qualify the variable.)

Having done the setup, we'll proceed from the bottom up, and include a subroutine to set up the connection to the remote computer.

sub establish_connection {
  use Socket;

  # Declare variables to keep
  #    'use strict' from whining.
  #    Here, we declare them all
  #    at the top of the routine.

  my $s_host = shift;
  my($c_host, $c_socket, $c_addr);
  my($s_addr, $s_socket, $port);

  my $pnum;
  my $pname = 'tcp';
  my $stype = SOCK_STREAM;

We use Socket to include the interfaces for interprocess communications, and then we declare all the variables we need, beginning by collecting the socket host, $s_host, from the argument list. We finish this by declaring the protocol and socket type we'll use.

Next, we'll get the protocol number from /etc/services, and establish the socket.

  # Make the socket filehandle.
  $pnum = getprotobyname $pname;
  socket(S, PF_INET, $stype, $pnum)
    or die "socket: $!";
  warn "socket ok" if $d;

Following that, we set up the socket and its address, bind the socket to handle S, and issue more status messages if we have debugging turned on.

  # Give our socket an address.
  $c_host = (POSIX::uname)[1];
  $c_addr   = inet_aton $c_host
    or die "no address: $c_host";
  $c_socket = sockaddr_in(0, $c_addr);
    # 0 means let kernel pick
  bind(S, $c_socket) or die "bind: $!";
  warn "bind ok" if $d;

Lastly, we establish the connection with the other computer, and read not the daytime, but the time. The difference is that the daytime service gives us the local time on the remote machine as text, without the timezone, but the time service provides a Unix time_t word in binary form.

  # Call up the server.
  $port = getservbyname('time', $pname)
    or die "No port";
  $s_addr   = inet_aton $s_host
    or die "no address: $s_host";
  $s_socket = sockaddr_in($port, $s_addr);
  connect(S, $s_socket) or die "connect: $!";
  warn "connect ok" if $d;
}

Now that we've got a routine to do the communication, we need a way to set the time on our local machine to match the time on the remote machine.

sub synchtime {
  # Here, we declare variables as they're used.

  my $rtime = shift;
  my $SECS_of_70_YEARS = 2208988800;
  #  From 1900 to the Epoch
  #  70*365*24*60*60 (70 regular years)
  #  + 17*24*60*60 (17 leap-days)

  my $histime = unpack("N", $rtime)
      - $SECS_of_70_YEARS ;
    # "N" is network-order long
  my $settime = POSIX::ctime $histime;
  chomp($settime);

We are assuming that we take the time_t as an argument, and our task is to decode that word. We need to know now many seconds in 70 years to enable us to correct the clock for the Unix epoch, 1970 January 1. Next, we use the Perl unpack() function to expand the word we got across the network, and (after correcting for the epoch) use that value to get the ASCII rendition of the current time. We finish up this paragraph of code by removing the trailing newlines from $settime. The chomp operator is a slightly safer version of the older Perl chop operator, because it ensures the character it's chopping is actually a newline.

What good does it do us to have the remote date in text format? It's a convenient form to feed to the GNU date utility. Why use the GNU version? Because every version of date we looked at requires its arguments for the -s flag (to set the date) in a slightly different form: some have seconds separated by a dot, some have the year preceeded by the century, some prohibit century. (POSIX.2 specifies the format specifiers for outputting the date, but not the format for inputting it.) We use the common, and widely-available, GNU version to provide a lingua franca date format.

   # assume GNU 'date'
  my $cmd =
   "/usr/local/bin/date -s '$settime'";
  warn $cmd if $d;
  my $rc = system($cmd);
  warn "system($cmd) failed" if ($rc != 0);
}

By the way, because GNU date allows you to set the date in nearly any format, it has the interesting feature of being a date format translator. For example, if I say

date +"%D" --date "31 Oct 1996"
I get 10/31/96 back. Even better, if I say
date --date "a week ago"
date responds 10/22/96. This improves the usefulness of the utility by a large measure.

This leaves us with the main program to write. Given the subroutines we've already written, that's pretty short:

my $s_host = shift || 'localhost';

establish_connection $s_host;
chomp($_ = <S>);
warn $_ if $d;
synchtime $_;
close (S) or die "close: $!";
exit 0;
We begin by getting the target off the command line. Next, we use our first subroutine to get the connection to the remote host, providing status if we have debugging turned on. Lastly, we set the local time, and finish.



The man page.

We've always been fascinated by things like self-replicating programs. So the notion of including the documentation in the program itself is a natural trick for us. We can do this in Perl because there are some commands that are valid in both troff and Perl .

For example, we begin our script with the lines:

'di';
'ig 00 ';
#
# $Id: synchtime,v 1.11 1996/10/09 ....
The di and ig lines are simple string declarations to Perl, and tell troff to divert and ignore the text until a line containing .00.

Then, we finish the Perl code with the lines:

exit 0;
.00 ;
'di
.nr nl 0-1
.nr % 0
'; __END__
The first two of these lines exit from the Perl program, and then complete the lines troff is ignoring. We begin a new Perl string declaration that contains the troff directives to close the diversion, and pretend that we're starting the first page of the man page again. Finally, we declare the end of the Perl text, and the beginning of the documentation text. This trick works in large part because a delimiter for Perl strings is also one of the two valid characters for starting a troff directive.

(We also occasionally use a similar technique in our C code, and bodily include the man page surrounded by #ifdef DOC and #endif. As we've discussed in these pages before, we still disagree about the utility of Don Knuth's literate programming technique.)

In any event, the man page text we include is:

SYNCHTIME(1)
NAME
synchtime - synchronize the system clock with another machine on the net
SYNOPSIS
synchtime [-d] hostname
DESCRIPTION
Synchtime uses the network 'time' service (port 37) to get the time from the named host, then calls GNU's date -s to set the time on the local machine.
FILES
/etc/hosts, /etc/services
AUTHOR
Jeffrey S. Haemer
SEE ALSO
date
DIAGNOSTICS
The -d flag produces some debugging information.
Whines if you don't run it as root, but doesn't do any damage.
BUGS
Must be run as root.
Requires GNU date because of the wild variety of formats that various vendors use for setting dates. (Unfortunately, POSIX.2 specifies formats for getting dates but not for setting them.) Luckily, GNU date is both widely available and has the best format going. Fortunately, GNU's date -s will let you give it almost any reasonable, verbose format, such as those returned by ctime().
It would be nice to use the "daytime" service (13), but the format returned doesn't give a time zone.


More follies in the time libraries.

An anonymous correspondent at QMS points out that our chums at Sun goofed in SunOS 4.1, et seq. SunOS doesn't provide the standard ANSI C mktime() routine to convert a struct tm into a time_t value. Since mktime() hadn't been standardized yet, SunOS provides similar functionality with timelocal(). In principle,,

mktime(localtime(time(NULL)))
should be the same as
time(NULL).
Unfortunately, the Sun routine ignores the tm_isdst flag in the struct tm, so during the summer, time isn't exactly invertible using
timelocal().

Keep those cards and letters coming, folks. We're enjoying collecting these glitches.

That's all for now. Next month, we'll be doing another column on HTML. This time, we'll discuss CGI techniques, and show you a bit of software we wrote for our eldest daughters' geography drills. And how we extended it to count electoral votes for the recent U.S. Presidential election. If history is any guide, this will generate a bunch of correspondence.

We ended up writing this column twice. We committed the sort of mistake that two professional nerds who've been at this for more than half a century shouldn't have made: one of us had a brain-skip and typed

rm 22.mm
instead of
rm 22.ps
as we were nearing completion. So, in the column following the HTML/CGI exploration, we'll discuss adding failsafe protection to rm.

Until then, happy trails.