Saturday, January 31, 2009

Ping or die

So, we've got this computer that we use as a router... and since it probably eats electricity like crazy, compared to an ordinary router, every night when everyone goes off-line somebody has to turn it off. And since programmers are lazy, we've been kicking around the idea of making a program that turns the damn thing off by itself.

But since programmers are lazy, I only did it today, after a year of excuse finding (at which I am highly proficient).

Result: here's a little Perl script to run commands when some computers go off-line. It turned out quite simple. Enjoy!

 
1 #!/usr/bin/perl
2 #
3 # Ping or die
4 #
5 # Pings all ip addresses and turns off the machine when
6 # all of them are dead.
7 # If you need to run other commads, you can do that too.
8 #
9 # Parameters:
10 # c - command to run,
11 # s - sleep time between pinging (in seconds),
12 # t - ping timeout (in seconds),
13 # IP addresses or hostnames to watch.
14 # Example:
15 # Turn on the Rokudan when the router's down for a minute:
16 # ping_or_die -s 60 "mpg321 Rokudan.mp3" 10.0.0.2
17 # Author:
18 # Konrad Siek
19
20 # Packages
21 use Net::Ping;
22 use Getopt::Std;
23
24 # Defaults
25 my $command = "shutdown -h now";
26 my $sleep_time = 300;
27 my $timeout = 10;
28 my $hosts = ();
29
30 # Setup options
31 my %options = ();
32 getopt "cst", \%options;
33 $command = @options{c} if defined @options{c};
34 $sleep_time = @options{s} if defined @options{s};
35 $timeout = @options{t} if defined @options{t};
36 @hosts = @ARGV;
37
38 # If no hosts were provided print help and exit.
39 if (scalar @hosts < 1) {
40 print "Usage: $0 [options] list-of-ips\n";
41 print "\t-c\tcommand to run\n";
42 print "\t-s\tsleep time between pinging (in seconds)\n";
43 print "\t-t\tping timeout (in seconds)\n";
44 exit -1;
45 }
46
47 # Setup ping - 30 second timeout
48 $ping = Net::Ping->new("tcp", $timeout);
49
50 # Main program
51 my $again;
52 do {
53 $again = 0;
54 # Wait some time before checking
55 sleep $sleep_time;
56 # Ping machines
57 foreach (@hosts) {
58 my $present = $ping->ping($_);
59 $again |= $present;
60 }
61 } while ($again);
62
63 # Finish up ping
64 $ping->close();
65
66 # Info
67 print "$0: dead hosts: @hosts\n";
68 print "$0: running command '$command'\n";
69
70 # Run command
71 system $command;
72


The code is also available at GitHub as perl/ping_or_die.

Thursday, January 1, 2009

Deth

Here's a new script for the new year.

It sometimes happens when I need to, say generate a dot file from a SableCC grammar to have a nice little diagram showing me how the damn thing is supposed to work. When you do one of those, not only does it tent to facilitate using your laptop as a simple toaster, or perhaps a very expensive heating system, but also usually eats up a lot of time. Something like 7 hours, for instance, and you don't know at all when it's going to end... or if it's going to generate anything, or maybe die out of lack of memory.

So it's not something that you want to wait up for until 2 AM - just let it run on it's own, and go to sleep instead. Good plan, yeah?

On the other hand, maybe it's wise to take those electricity bill into account? How about turning it off right after the process finishes (or dies)?

Ah, and did you have the foresight to actually write the shabang which shuts the computer down when the process is done? I never do...

That's where this script comes in. You just tell it to watch the process, and do stuff if it happens to die. It's as simple as that.

So, in the particular case that I described, I'd use it like this:

sudo ./deth sablecc "shutdown -h now";


And it won't work. The script is not prepared to handle the sudo-ing out of the box. Why? Well... you know, because I suck. But it's not a problem, all you need to do is take a peek at line 40 and change `whoami` to just your username, for instance: joe (with no spaces, no inverted commas, no nothing... well, double quotes, if you have to). Problem solved.

There's another variable that you can handle as well, in line 37. The sleep time between checks is here, in seconds. If 5 minutes doesn't cut it for you, modify it right there; no worries, she'll be right.

Yeah, for some reason I didn't feel like doing the entire getopt thing, and make these things settable... that's laziness, that is.

Here's the code.
 
1 #!/bin/bash
2 #
3 # Deth
4 #
5 # A watchdog for the death of processes. After specifying
6 # the list of processes to watch (either by name of PID)
7 # the script waits for them to turn off, in which case the
8 # specified command is run (and sent into the background).
9 # When all watched processes die, the script exits.
10 #
11 # The commands are translated into PIDs at the beginning
12 # only, so if you want to watch gcalctool, and, while this
13 # script is already running, turn on another instance of
14 # gcalctool, the new instance will be completely ignored.
15 #
16 # The script checks for the death of the specified processes
17 # and then goes to sleep for a specified period of time.
18 # Since this does not need to be a very precise script, it's
19 # currently set to checking every 5 minutes - this can be
20 # changed by modyfing the value of SLEEP_TIME.
21 #
22 # The script only watches processes which belong to the
23 # current user. To modify this set the value of USERNAME.
24 #
25 # Potential issues:
26 # When selecting by command name, all fitting PIDs
27 # are watched, without any further discrimination.
28 # Parameters:
29 # List of running commands and/or PIDs.
30 # The last argument is always the command to run when
31 # each of them turns off.
32 # Author:
33 # Konrad Siek
34 #
35
36 # How long to wait between pings. In seconds.
37 SLEEP_TIME=300
38
39 # Limit the watched processes to the current user.
40 USERNAME=`whoami`
41
42 # Check if enough arguments to even try getting the PIDs.
43 if [ $# -lt 2 ]
44 then
45 echo -e "Usage: \n\t$0 [pids] [commands] action\n"
46 exit -1
47 fi
48
49 # Convert all arguments to a PID array
50 declare -a array
51 while [ "$2" != "" ]
52 do
53 if [ `expr "$1" : "[0-9][0-9]*"` != 0 ]
54 then
55 # Add PIDs to the array as they are.
56 if [ $(\
57 ps U $USERNAME -u $USERNAME -o pid \
58 | grep "^ *$1 *\$" | wc -l) != 0 ]
59 then
60 array=( ${array[@]} $1 )
61 else
62 echo "PID $1 is not valid, ignoring." >& 2
63 fi
64 else
65 # Extract PIDs from name and add them to the array.
66 array=( ${array[@]} $(\
67 ps U $USERNAME -u $USERNAME -o pid,comm \
68 | awk -v f=$1 '$2==f {printf($1" ")}' ) )
69 fi
70 shift
71 done
72
73 # The last parameter is the action done on death of processes.
74 ACTION=$1
75
76 # No array can be generated - no PIDs were discerned.
77 if [ ${#array[@]} = 0 ]
78 then
79 echo "No PIDs were supplied." >& 2
80 exit -2
81 fi
82
83 # Work the magic.
84 while [ 1 ]
85 do
86 # Check if anything changed, and act.
87 i=0; len=${#array[@]}
88 while [ $len -gt 0 ]
89 do
90 # Ignore empty elements.
91 if [ "${array[$i]}" = '' ]
92 then
93 # Point to next element
94 i=$(($i + 1))
95 continue
96 fi
97
98 # Fewer elements to visit left.
99 len=$((len - 1))
100
101 # Check if process dies.
102 if [ $(\
103 ps U $USERNAME -u $USERNAME -o pid \
104 | grep "^ *${array[$i]} *\$" | wc -l) = 0 ]
105 then
106 # Process is dead, execute action.
107 $ACTION &
108 unset array[$i]
109 fi
110
111 # Point to next element
112 i=$(($i + 1))
113 done
114
115 # If nothing left to do, quit.
116 if [ ${#array[@]} = 0 ]
117 then
118 exit 0;
119 fi
120
121 # Wait a bit before continuing.
122 sleep $SLEEP_TIME;
123 done


The code is also available at GitHub as bash/deth.

P.S. I hate those gorram, depressing date changes.