#!/usr/bin/perl
#: wait until a certain time, or until a process ends
#======================================================================
# Time-stamp: "2012-10-31 00:50:04 MDT sburke@cpan.org"

require 5;
use strict;
use constant DEBUG => 0;
use warnings;
my @pids;
my $pause;

# usage:   after 10m8s
#            (sleeps for whatever number of seconds that is)
#     or   after 23355 342 342 636
#            (wait until all these processes are done)
#          after 3253 'foobar'
#            (wait undel process 3254, and all processes
#             whose ps listing contains 'foobar', are done)
#
# You can mix substrings and process numbers, but a time (10:30) or a
# duration (22m, 9s, 2m18s, 10h9m, etc.) can't be mixed with
# anything else.
#
##======================================================================
#
# Notes, some time later: if I had it to do all over again, I probably
# wouldn't use these ps(1) calls-- I'd look at /proc/ instead.
#
#======================================================================

@^T = localtime $^T;
use constant     DAY_S => 24 * 60 * 60;
use constant HALFDAY_S => 12 * 60 * 60;

my $to_list_them_all = 'ps -e -o pid= -o args';  # was just "ps ax"
my $to_check_running = 'ps -o pid='; # was just "ps"

{
  my $self = $$;
  my %active;
  foreach ( qx{$to_list_them_all} ) {
    #print ">$_";
    if(/^\s*(\d+)\s([^\cm\cj]+)/os) {
      $active{$1} = $2 if $1 != $self;
    }
  }
  
  DEBUG and print scalar(keys %active), " actives\n";
  my($num,$line, @matching);
  foreach my $x (@ARGV) {
    if($x =~ m<^\d+$>) {
      die "Can't specify a process along with a pause-time!\n" if defined $pause;
      die "No such active process pid as $x\n"
       unless exists $active{$x += 0};
      push @pids, $x;
    } elsif (length $x and
      $x =~
        m<^
           (?:(\d+)d)?
           (?:(\d+)h)?
           (?:(\d+)m)?
           (?:(\d+)s)?
          $
         >sx
      and defined $+ # sanity check
    ) {
      die "Can't specify a pause-time along with a process!\n" if @pids;
      $pause =
        ( ($1 || 0) * 86400 ) +
        ( ($2 || 0) *  3600 ) +
        ( ($3 || 0) *    60 ) +
        ( ($4 || 0)         )
      ;
    } elsif (length $x and     # It's a time-of-day thing
      $x =~
        m<^
           ( [012]?        # h, one digit or two
             \d)
           (?:
             \:
             ([012345]\d)  # m
           )?
           (?:
             ([ap])
             \.?m\.?       # suffix
           )?
          $
         >sx
    ) {
      die "Can't specify an at-time along with a process!\n" if @pids;
      $pause = seconds_until($x, $1,$2,$3);
    } else {
      die "Can't specify a process along with a pause-time!\n" if defined $pause;
      @matching = ();
      while(($num,$line) = each %active) {
        DEBUG > 10 and print "No $x in $line\n";
        next if -1 == index($line, $x);
        print "$num << $line\n";
        push @matching, $num;
      }
      die "\"$x\" matches no active processes\n" unless @matching;
      push @pids, @matching;
    }
  }
}

$| = 1;

if(defined $pause) {
  print "Sleeping for ", d_h_m_s($pause), ", until ",
    scalar(localtime($^T + $pause)), "\n";
  exec("sleep $pause") || die "Couldn't exec?";
}

print "Waiting for [@pids] to finish...\n";

my(@new_pids);
while(@pids) {
  @new_pids = ();
  DEBUG and print "  checking for [@pids]...\n";
  foreach my $line ( qx{$to_check_running @pids} ) {
    if(  $line =~ m/^\s*(\d+)\s/ ) {
      push @new_pids, $1;
      DEBUG > 9 and print("OK, continuing $1 because of $line\n");
      DEBUG > 3 and print " Hit: $line";
    }
  }
  @pids = @new_pids;
  sleep 5;
}
DEBUG and print "Okay, exiting...\n";
exit;

#======================================================================

sub hm2time {
  my($h,$m) = @_;
  return timelocal(0,$m,$h, @^T[3,4,5]);
}
sub hour_pm_to_24h {
  my $h = $_[0];
  return 12 if $h == 12;   # 12pm = 12
  return 12 + $h;          # 1pm  = 13, etc
}
sub hour_am_to_24h {
  my $h = $_[0];
  return 0 if $h == 12;   # 12am = 0
  return $h;              # 1am  = 1, etc
}

sub seconds_until {
  my($wholething, $h, $m, $suffix) = @_;
  $m ||= "0";
  DEBUG and print "$wholething / $h : $m ", $suffix || '', "\n";
  my @possibles; # possible times
  use Time::Local;

  if( $h > 12 or $h =~ m<^0\d$>s ) { # it's military time
    $h = 0 if $h == 24;
    DEBUG > 2 and print "H= $h (via military)\n";
    push @possibles, hm2time($h,$m);

  } elsif($suffix) {
    $h = (lc $suffix eq 'p') ? hour_pm_to_24h($h)
                             : hour_am_to_24h($h);
    DEBUG > 2 and print "H= $h with suffix $suffix, from $wholething\n";
    push @possibles, hm2time($h,$m);

  } else {
    # No suffix. Consider both.
    DEBUG > 2 and print "H= $h nosuffix\n";
    push @possibles, hm2time($h,$m);
    push @possibles, $possibles[-1] + HALFDAY_S;
  }
  push @possibles, map $_ + DAY_S, @possibles;

  @possibles =
    sort {$a <=> $b}
      grep {$_ > $^T} @possibles;

  die "\"$wholething\" denotes no possible times!?!?\nAborting"
   unless @possibles;

  DEBUG and print "Possibles:\n",
                    (map "\t$_ = ".scalar(localtime($_))."\n",
                      @possibles), "\n";

  my $then = $possibles[0]; # earliest possible
  my $waiting = $then - $^T;
  DEBUG and print "Interpreting $wholething as ",
      scalar(localtime($then)), "\n which is ",
      $waiting, " seconds from now.\n";

  #DEBUG > 9 and die;
  return $waiting;
}

#======================================================================

sub d_h_m_s {
  # Take seconds, and return a friendly string representation of it.
  my $in = $_[0] + 0;
  my($d, $h, $m, $s) = to_d_h_m_s($in);
  if($d) {
    return sprintf("%02dd %02dh %02dm %02ds", $d, $h, $m, $s);
  } elsif($h) {
    return sprintf("%02dh %02dm %02ds",$h, $m, $s);
  } elsif($m) {
    return sprintf("%02dm %02ds", $m, $s);
  } else {
    return sprintf("%02ds", $s);
  }
}


sub to_d_h_m_s { # convert to days, hours, minutes, seconds
  my($in) = $_[0];
  my($days, $hours, $minutes);
  $days = int($in / 86400);  $in %= 86400; # shave off days
  $hours = int($in / 3600);  $in %= 3600;  # shave off hours
  $minutes = int($in / 60);  $in %= 60;    # shave off minutes
  return ($days, $hours, $minutes, $in); # "in" = seconds by now
}

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



__END__
