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= }