Copyright Notice

This text is copyright by CMP Media, LLC, and is used with their permission. Further distribution or use is not permitted.

This text has appeared in an edited form in WebTechniques magazine. However, the version you are reading here is as the author originally submitted the article for publication, not after their editors applied their creativity.

Please read all the information in the table of contents before using this article.
Download this listing!

Web Techniques Column 39 (Jul 1999)

[suggested title: Countdown to Y2K]

Almost every day, I'm hearing another story in the popular or industry press about Y2K. Y2K compliance for the air traffic control. Y2K concerns about the power grid. And of course, a frequently asked question about Perl is ``does the XXX Perl module handle Y2K properly?''. Well, ultimately, who knows? But in the meanwhile, I've got some code to hack out.

One of the things that seem to be popping up everywhere are these cute little Y2K countdown clocks. For the most part, they seem to be Java or Javascript toys, all completely useless after the turn of the century. So, it occurred to me that I could write one of these gadgets without resorting to such overkill. And, at the same time, I could show off how to create a dynamically changing image using ``server push'' technology -- something most modern browsers understand just fine.

Now, most of the things that server push had been used for has now been subsumed by the proper creation of ``animated GIFs'', but this works only when all the images can be predetermined before the download has begun. But in the case of the Y2K clock, we need to compute each image individually, because the clock is never the same twice.

In order to use a server-push script, we've got a few restrictions. One is that we'll need to generate a repeated set of HTTP headers and bodies, all within a structure that gives the overall content-type of multipart/x-replace-mixed. We'll also need to write at times of our own choice to the browser; we'll do this by making the CGI program an ``NPH'' script, and also disabling Perl's output buffering.

Many modern versions of Apache (the world's number one server) no longer need the script to be specifically designated as ``NPH'', although there's no harm in keeping this script compatible with older servers.

So, once I had an idea of the implementation technology and restrictions, I hacked out the program given in [listing one below].

Lines 1 through 3 start nearly every program I write of any length. I'm enabling ``taint'' mode (useful for CGI scripts to protect me from my own gross blunders), turning on warnings, enabling compiler restrictions, and unbuffering STDOUT. That last item is very important in an NPH script, because I want the output showing up as quickly as possible, which buffering would defeat.

Line 5 pulls in the standard CGI.pm module, and brings in the ``standard'' shortcuts. We'll use this to get the lone parameter (timezone), and also generate the right header for the various GIFs.

Line 6 pulls in the GD.pm module, so that I can create simple GIFs. In this case, I'll be creating a GIF that's entire made of text, but GD doesn't care. This module is found in the Comprehensive Perl Archive Network (CPAN), and is easily installed from there.

Line 8 defines a nice utility constant with a return/linefeed pair. Lines 9 and 10 define two configuration constants: how long to wait between clock updates, and how many times to update the clock. We want to limit the number of times; otherwise a browser could tie up our web server indefinitely. With the settings defined here, the clock will run for about 5 minutes before timing out.

Line 13 defines a constant equivalent of midnight on the fateful day, expressed in the internal Unix time value (seconds since January 1st, 1970, at midnight GMT). We'll use this as our target countdown time, offset by the selected timezone. Line 12 shows how to compute this value from a simple Perl one-liner, invoking the Date::Manip module (found in the CPAN). You don't think I would have actually figured that out by hand, do you?

Lines 14 through 21 define a series of units (from largest to smallest), along with their number of seconds equivalents. We'll use this table to down-convert from seconds to the actual time. Note that since the Y2K date is less than a year away as I write this, the year value won't be used, but I threw it in there anyway.

Line 23 fetches the parameter for tz, the timezone for which this report should be accurate. By default, the value is 0, and thus the counter will report the information relative to midnight GMT. For my home of Portland, Oregon, I'll use a value of 8 (except in the summer it's 7), and the numbers will reflect my localtime. To be pessimistic, you could put a timezone of -13 or so, and cover it from the very first part of the Earth turning Y2K.

Lines 25 through 27 select a font for GD's output, and determine some basic font characteristics (the width and height of a single character).

Line 29 prints the NPH header, required by the HTTP protocol. Since we are more or less speaking directly to the web browser, we've had to add the extra header to indicate the HTTP status.

Line 31 defines a constant for the multipart output boundary. This sequence of characters must not occur in the output stream, or we'll run into trouble.

Lines 32 through 35 dump out the HTTP header, declaring the response to be a multipart/x-mixed-replace (the type of ``server push''), and starting the first boundary for the first part.

Lines 36 through 45 define a loop, executed once for each countdown update. The variable $count is unused inside the loop, and serves merely as a way for me to exit the loop after the appropriate number of times.

Line 37 invokes the time_gif function, defined later, to create the bytes of a GIF image for the current countdown time of day.

Line 38 through 41 dump out the multipart HTTP header. This header needs to look just like a normal header, except that the information applies only to the part being dumped. The content-type is image/gif, which is the appropriate type for a GIF file. The content-length in general should be specified; otherwise, data looking like the characters making up the boundary marker might be mistaken for the actual boundary. (I've heard of browsers that confuse the boundary even with a content-length specified, but I've not seen one in practice. Just make your boundary string unlikely enough, and don't worry about it.) Note that an expiration of now is also included. This is needed so that the rendering engine doesn't keep a cached value of the GIF around. I found that Internet Explorer was picky about this (at least, when I had my cache setting at once per session), but adding the Expires header fixed it right up.

Line 42 dumps the actual GIF, now that we've gotten out of the header. Line 43 follows this up with the boundary. Once the boundary is seen after the content has been read, the browser is free to display the information as if it had seen an entire transaction. But, we're still connected, and line 44 sends us away for a little while until it's time to generate another update.

Line 47 finishes the program. This line is rarely reached; most people just abort the web page before we get here.

Lines 49 through 76 define the time_gif function, which returns a GIF image of text representing how long it is before the Y2K rollover. That time is computed in lines 50 and 51, stored as seconds into $left. We'll use the timezone to first offset the Unix timestamp (from time) by that many hours, then subtract that from Y2K at midnight GMT.

Lines 52 through 64 turn the time left into a string. First, we'll declare the string in line 52, and default it to a value that'll work after the Y2K date has passed. But if we're still on the early side, we have time to compute the remaining time.

Lines 54 through 59 take the @TIME_UNITS hash, and transform it into a list of units appropriate for the value in $left. Each item of this list is an arrayref, which gets dereferenced and deposited into $unit and $secs in line 55. Then line 56 determines the whole number of those units in $left. Line 57 subtracts those units out, and line 58 constructs a singular or plural value as appropriate. If a particular unit is 0, it completely disappears from the final list.

Lines 60 through 63 hook together the pieces of the resulting units list. The three cases distinguish what to do if there is only one unit, only two units, or more than two units, putting the right number of commas and possibly the word and in the right place.

Once we've got a good string to display in $string, it's time to build the GIF using GD. Lines 66 and 67 compute the size of this image, based on the length of the string and the size of a single character.

Lines 69 through 75 create the blank GIF, define a background color (the first color allocated is always the base background color), set the image to transparent (using the selected background color), turn on interlacing (not needed on such small GIFs, but handy for larger ones), and display the string using a nice pure red color. Finally, the image is turned into a GIF, and that becomes the return value of this subroutine.

So, to use this server-push image generator, I'll stick the script into my CGI bin area. Let's say it's accessible as /cgi/nph-y2kcounter. Next, I'll get the proper timezone (8 for Pacific Time), and embed a reference to this counter as an image in an HTML file:

        <HTML><HEAD><TITLE>Y2K!</TITLE></HEAD>
        <BODY><H1>Y2K!</H1>
        Run for the hills, you have just
        <IMG SRC="/cgi/nph-y2kcounter?tz=8">
        left!
        </BODY></HTML>

Note the ?tz=8 after the name of of the script, which will show up as a parameter of 8 in the script.

There's nothing that restricts only one embedded graphic in an HTML file like this. In fact, I was playing with an earlier version of this program that generated an analog clock, and created one of those ``timezones across the US'' displays using four separate clocks as four server pushes. Of course, the downside of this is that each clock keeps one webserver tied up, but it still looked cool.

One way to avoid the server tie up is to use a ``client pull'' script instead of a server-push script. For this, you'll include the proper refresh HTTP header along with a single image in the normal fashion. Then, after the requested number of seconds, the browser automatically re-fetches the same URL. The downside of this method is that your browser will have to reconnect for each update, and the CGI script must relaunch. Depending on your sophistication, you could create a mini-web-server as I've described here in a few of the prior columns to get around this problem.

So, time flies, and now you'll now exactly how much time is being wasted before the big rollover. Until then, enjoy!

Listings

        =1=     #!/usr/bin/perl -Tw
        =2=     use strict;
        =3=     $|++;
        =4=     
        =5=     use CGI qw(:standard);
        =6=     use GD;
        =7=     
        =8=     use constant CRLF => "\015\012";
        =9=     use constant SLEEPYTIME => 5;
        =10=    use constant MAX_UPDATE => 60;
        =11=    
        =12=    ## perl -MDate::Manip -e 'print &UnixDate(ParseDate("1/1/2000 12:00am GMT"),"%s")'
        =13=    use constant Y2K_GMT => 946684800;
        =14=    my @TIME_UNITS = (
        =15=                      [ year => 365.25 * 24 * 60 * 60 ],
        =16=                      [ week => 7 * 24 * 60 * 60 ],
        =17=                      [ day => 24 * 60 * 60 ],
        =18=                      [ hour => 60 * 60 ],
        =19=                      [ minute => 60 ],
        =20=                      [ second => 1 ],
        =21=                     );
        =22=    
        =23=    my $tz = param('tz') || 0;
        =24=    
        =25=    my $font = gdLargeFont;
        =26=    my $char_x = $font->width;
        =27=    my $char_y = $font->height;
        =28=    
        =29=    print "HTTP/1.0 200 OK" . CRLF;
        =30=    
        =31=    my $BOUNDARY = "the-time";
        =32=    print map($_ . CRLF,
        =33=              "Content-Type: multipart/x-mixed-replace; boundary=$BOUNDARY",
        =34=              "",
        =35=              "--$BOUNDARY");
        =36=    for (my $count = 1; $count < MAX_UPDATE; $count++) {
        =37=      my $gif = time_gif();
        =38=      print header(-type => 'image/gif',
        =39=                   -content_length => length($gif),
        =40=                   -expires => 'now',
        =41=                   );
        =42=      print $gif;
        =43=      print CRLF . "--$BOUNDARY" . CRLF;
        =44=      sleep SLEEPYTIME;
        =45=    }
        =46=    
        =47=    exit 0;
        =48=    
        =49=    sub time_gif {
        =50=      my $time = time - 3600 * $tz;
        =51=      my $left = Y2K_GMT - $time;
        =52=      my $string = "no time at all";
        =53=      if ($left >= 0) {
        =54=        my @items = map {
        =55=          my ($unit, $secs) = @$_;
        =56=          my $count = int($left/$secs);
        =57=          $left -= $count * $secs;
        =58=          $count ? $count == 1 ? "1 $unit" : "$count ${unit}s" : ();
        =59=        } @TIME_UNITS;
        =60=        $string =
        =61=          @items > 2 ? join(", ", @items[0..$#items-1], "and $items[-1]") :
        =62=            @items > 1 ? "$items[0] and $items[1]" :
        =63=              $items[0];
        =64=      }
        =65=    
        =66=      my $picture_x = $char_x * length($string);
        =67=      my $picture_y = $char_y;
        =68=    
        =69=      my $image = GD::Image->new($picture_x, $picture_y);
        =70=      my $background = $image->colorAllocate(127,127,127);
        =71=      $image->transparent($background);
        =72=      $image->interlaced('true');
        =73=      my $red = $image->colorAllocate(255,0,0);
        =74=      $image->string($font, 0, 0, $string, $red);
        =75=      $image->gif;
        =76=    }

Randal L. Schwartz is a renowned expert on the Perl programming language (the lifeblood of the Internet), having contributed to a dozen top-selling books on the subject, and over 200 magazine articles. Schwartz runs a Perl training and consulting company (Stonehenge Consulting Services, Inc of Portland, Oregon), and is a highly sought-after speaker for his masterful stage combination of technical skill, comedic timing, and crowd rapport. And he's a pretty good Karaoke singer, winning contests regularly.

Schwartz can be reached for comment at merlyn@stonehenge.com or +1 503 777-0095, and welcomes questions on Perl and other related topics.