Awaiting Death

In which I coerce processes to email me as they die.

Tagged: Software

I’ve been running a number of experiments recently that require a lot of computing time. “A lot” in this case being on the order of days. It would therefore be nice to have a script that would automatically E-mail me when my experiments finish, so I know to check the results. I fully expected there to be some magic shell script out there somewhere dedicated to this very purpose: sending out an E-mail when a specified process dies. Something like this:

$ ./run_experiments&
[1] 1337
$ emailwhendone 1337
Awaiting process 1337's death...

As far as I can tell, however, there is no such script/program. So, as usual, I took it upon myself to write my own. The E-mailing part turned out to be a bit trickier than I had expected.

I didn’t want my script to be dependent on the existence of a local mail server; therefore, I first tried using sSMTP. It turns out that sSMTP requires one to hard-code the remote SMTP server address in a .conf file, so that approach was out.

Next I tried Mail::Sendmail, however, that module’s support for authentication is poor at best. That module also doesn’t support SSL, so emailing through servers like Google Mail is out.

Therefore, I finally settled on using Net::SMTP::SSL, which unfortunately has four dependencies. Luckily for me, those dependencies are all easily installable on Gentoo:

  1. dev-perl/Authen-SASL
  2. dev-perl/IO-Socket-SSL
  3. dev-perl/Net-SSLeay
  4. dev-perl/Net-SMTP-SSL

I call my script emailwhendone because, well, that’s exactly what it does. The code follows at the end of this post.

Disclaimer: I blatantly cribbed some of my code from Robert Maldon (for the MTA stuff) and Bill Luebkert (for the password input).

The script can be given one of two parameters: either the PID of the process for which to wait or the unique name of the process (if there are multiple processes with the same name you will need to use the PID). Right now I have the recipient E-mail address hard-coded; it should be fairly self evident from the code how to customize this. Here’s an example:

$ ./run_experiments&
[1] 1337
$ emailwhendone 1337
Password for [email protected]: *******************
Waiting for process 1337 (run_experiments) to finish...
The process finished!
Sending an email to [email protected]...
$

Here’s the code:

#!/usr/bin/perl -w

use Net::SMTP::SSL;
use Term::ReadKey;	END { ReadMode ('restore'); }	# just in case

my $destination = '[email protected]';
my $server = 'smtp.domain.com';
my $port = 465;

#####################################

sub usage {
    print " Usage: emailwhendone [PID|PROCESS_NAME]\n";
}

my $pid = $ARGV[0] or die &usage();
my $hostname = `hostname`;
my $pidmatch = -1;
my $processmatch = "";
my @pidmatches;

open PRO, "/bin/ps axo pid,comm |" or die 'Failed to open pipe to `ps`';

while() {
    if($_ =~ m/^\s*(\d+)\s+(.+)$/) {
        my $matchpid = $1;
        my $matchprocess = $2;
        if($matchpid eq $pid) {
            $pidmatch = $matchpid;
            $processmatch = $matchprocess;
            @pidmatches = [$matchpid];
            last;
        } elsif($pid =~ m/^\s*$matchprocess\s*$/) {
            $pidmatch = $matchpid;
            push(@pidmatches, $matchpid);
            $processmatch = $matchprocess;
        }
    }
}

close PRO;

if(scalar(@pidmatches) <= 0) {
    if($pid =~ m/^\s*\d+\s*$/) {
        print "Error: no process with ID " . $pid . "!\n";
    } else {
        print "Error: no process named \"" . $pid . "\"!\n";
    }
    exit(1);
} elsif(scalar(@pidmatches) > 1) {
    print "There are multiple PIDs that match this process name!\n";
    for my $match (@pidmatches) {
        print $match . "\t" . $pid . "\n";
    }
    exit(2);
}

sub get_passwd {
    # legal clear passwd chrs (26+26+10+24=86): "a-zA-Z0-9!#$%&()*+,-./:;<=> ?@[\]^";
    my @legal_clear = ('a'..'z', 'A'..'Z', '0'..'9', split //,
                       '!#$%&()*+,-./:;<=> ?@[\]^');
    my %legal_clear; foreach (@legal_clear) { $legal_clear{$_} = 1; }
    $| = 1;	# unbuffer stdout to force unterminated line out
    ReadMode ('cbreak');
    my $ch = '';
    while (defined ($ch = ReadKey ())) {
	last if $ch eq "\x0D" or $ch eq "\x0A";
	if ($ch eq "\x08") {	# backspace
            print "\b \b" if $passwd;	# back up 1
            chop $passwd;
            next;
	}
	if ($ch eq "\x15") {	# ^U
            print "\b \b" x length $passwd;	# back 1 for each char
            $passwd = '';
            next;
	}
	if (not exists $legal_clear{$ch}) {
            print "\n'$ch' not a legal password character\n";
            print 'Password: ', "*" x length $passwd; # retype *'s
            next;
	}
	$passwd .= $ch;
	print '*';
    }
    print "\n";
    ReadMode ('restore');
    return $passwd;
}

print "Password for " . $destination . ": ";
my $password = get_passwd();

sub send_mail {
    my $subject = $_[0];
    my $body = $_[1];
 
    my $smtp;

    if (not $smtp = Net::SMTP::SSL->new($server,
                                        Port => $port,
                                        Debug => 0)) {
        die "Could not connect to server.\n";
    }

    $smtp->auth($destination, $password)
        || die "Authentication failed!\n";

    $smtp->mail($destination . "\n");
    $smtp->to($destination . "\n");
    $smtp->data();
    $smtp->datasend("From: " . $destination . "\n");
    $smtp->datasend("To: " . $destination . "\n");
    $smtp->datasend("Subject: " . $subject . "\n");
    $smtp->datasend("\n");
    $smtp->datasend($body . "\n");
    $smtp->dataend();
    $smtp->quit;
}

print "Waiting for process " . $pidmatch . " (" . $processmatch . ") to finish...";

my $done = 0;
do {
    $done = 1;
    open PRO, "/bin/ps axo pid |" or die 'Failed to open pipe to `ps`';
    while() {
        if($_ =~ m/^\s*$pidmatch\s*$/) {
            $done = 0;
            last;
        }
    }
    close PRO;
    sleep(1);
} while(!$done);

print "The process finished!\nSending an email to " . $destination . "...";

&send_mail('Process ' . $pidmatch . ' (' . $processmatch . ') on ' . $hostname . ' finished!', 'It\'s done!');

print "\n";
← ישן יותר Blog Archive חדש יותר →