Tuesday, July 28, 2009

Episode #53: The Final Countdown

Ed starts:

My 40th birthday is coming up this Tuesday (the day that this episode will be posted), so I've been thinking about the passage of time a lot lately, with countdowns, hour glasses, and ticking clocks swirling through my thoughts. This all came to a head a couple of days ago, when I received an e-mail from a buddy of mine who runs capture the flag games. He wanted a countdown timer for his games, something to spice up players' experience in the game. But, instead of using some lame clock or stopwatch app, he asked me to provide him some command-line kung fu to do the trick. I came up with a solution that he liked, and it includes some fun twists and turns that I thought our readers here may find useful or at least enjoyable.

I relied heavily on FOR loops in the following command:

C:\> for /l %m in (1,-1,0) do @for /l %s in (59,-1,0) do @echo %m minutes %s seconds
LEFT & ping -n 2 127.0.0.1 > nul

1 minutes 59 seconds LEFT
1 minutes 58 seconds LEFT
1 minutes 57 seconds LEFT
1 minutes 56 seconds LEFT
1 minutes 55 seconds LEFT

Here, I've made a countdown timer that will run for two minutes (starting at minute 1, and counting back 60 seconds from 59 to 0, then going to minute zero and counting back the same way). I start off my minute variable (%m), running it from 1 down to 0 in steps of -1. If you want a 10 minute counter, replace that 1 with a 9. My second variable (%s) runs from 59 down to 0, again in steps of -1. At each iteration through the loop, I print out how much time is left. Finally, I ping myself twice, which takes about* a second (the first ping happens immediately, the second happens about* 1 second later).

That was my starting point. But, I wanted to add some flair, adding a little audio and popping up a message on the screen at the end of each minute. So, I added:

C:\> (for /l %m in (1,-1,0) do @(for /l %s in (9,-1,0) do @(echo ONLY %m minutes %s seconds
LEFT & ping -n 2 127.0.0.1>nul)) & start /max cmd.exe /c "echo ^G %m MINUTE^(s^)
LEFT! & ping -n 6 127.0.0.1>nul") & echo ^G^G^G^G^G^G^G^G^G


Here, as each minute expires, I'm using the start command to run a program in a separate window (which start does by default... the /b option makes start run a program in the backgroudn of the same window as we discussed in Episode #23 on job control) maximized on the screen with the /max option. The start command allows me to kick off something else while my main loop keeps running, ticking off seconds into the next minute.

The program that the start command will launch is cmd.exe, which I'm asking to execute a command with the /c flag. The command run by cmd.exe will echo a CTRL-G, which makes the system beep, and then echoes the number of minutes left. We then wait for 5 seconds (by pinging localhost 6 times). Because I started the cmd.exe with a /c option, after its command finishes (5 seconds later), the window will disappear. The location of all those parens is vitally important here, so that we're properly grouping our commands together to notch off time.

To add even a little more flair, I added several CTRL-Gs after the timer is done to make the expiration of the full time more audible.

* OK.... yes, you are right. The countdown timer I've describe here isn't super accurate, because the 1-second rule on pings is an approximation. Also, some time will be consumed by the commands gluing this all together, making this stopwatch slower than it should be. Also, if the system is heavily loaded, that'll slow things down even more. But, to a first approximation, we've got a stopwatch here. For more accuracy, you could write a script that relies on the %time% variable we discussed in Episode #49.

Time for Hal:

Emulating Ed's loop is straightforward. I'll even add a command at the end to pop up a window when time runs out:

$ for ((m=1; $m >= 0; m--)); do for ((s=59; $s >= 0; s--)); do \
echo $m minutes $s seconds to go; sleep 1; done; done; \
xterm -e 'echo TIME IS UP!; bash'

Notice that I'm executing bash in the xterm after the echo command. Spawning an interactive shell here keeps the xterm window from closing as soon as it echoes "TIME IS UP!".

But Ed's example got me thinking about ways to have a more accurate clock. One idea that occurred to me was to just use watch on the date command:

$ watch -dt -n 1 date

This will give you a clock that updates every second, with some extra highlighting to show the positions in the time string updating (that's the "-d" option). But this isn't a count down timer, it's a "count up" timer.

Another idea I had was to use at or cron to pop up the warnings (see Episode #50 for more detail on at and cron). The only problem is that both at and cron are limited to 1 minute granularity, so they don't work really well as a count down timer. But if you know your capture the flag session is supposed to end at noon, you could always do something like:

$ echo xterm -display $DISPLAY -e \'echo TIME IS UP\!\; bash\' | at noon

Notice I had to use a bunch of backwhacks so that the echo command passes a syntactically correct command into at. More interestingly, I have to explicitly declare my local $DISPLAY in the xterm command. While at is normally careful to preserve your environment variable settings, it apparently intentionally skips $DISPLAY-- probably because there's no guarantee you'll actually be on that screen when you at job finally executes.