eval '(exit $?0)' && eval 'exec perl -S $0 ${1+"$@"}'
  & eval 'exec perl -S $0 $argv:q'
  if 0;
#  THE PRECEEDING STUFF EXECS perl via $PATH
# -*-Fundamental-*-
# This is work in progress, but might serve as a good example of how a
# process can "tail" the Perforce journal to learn of interesting
# events.
#
# This script now allows for multiple entries in an Actions table (see
# below) which allows you to configure it to trigger different actions
# when particular journal entries occur.
#
use POSIX 'setsid';
use POSIX ':sys_wait_h';
my $p4;
if (-x "/a/tools/bin/p4")
  { $p4 = "/a/tools/bin/p4"; }
elsif (-x "/usr/local/bin/p4")
  { $p4 = "/usr/local/bin/p4"; }
else
  { die "no p4!"; }
my $p4port;
my $h = `/bin/hostname`;
if ($h eq "chinacat.foxcove.com\n")
  { $p4port = "chinacat.foxcove.com:1666 -u rmg"; }
else
  { $p4port = "perforce:1666 -u p4"; }
my $P4 = "$p4 -p $p4port";
$ENV{"P4CONFIG"} = "P4ENV";
#  This dispatch table talls us what to run when we see particular
#  entries in the journal. The default one below has been left in
#  mainly as an example; it can be overridden with the "-a
#  <actionsfile>" option. The special name "ddfab_*" causes p4jd to
#  add entries to the actions table based on information in the Branch
#  specs.
#
my $Actions_file;
my @Actions;
my @Branch_Actions;
#  Note to self: We no longer use the def_actions table below at Data
#  Domain; Rather, this ground in covered by /etc/sysconfig/p4jd.conf
#  on the p4jd server host. This the table below can be considered a
#  fossil example.
# 
#  On the other hand, the kludgey we we handle sync_module events that
#  have to run on other hosts (see the code in sync_module) still
#  mandates changes to the code in sync_module. Have I confused you
#  yet?  I've sure confused me!
#
sub def_actions
{
  @rawActions = split(/\n/, <<EOA);
#
#name                   type    db.file path                            chdir           action
#  
sync_tools              pv      db.rev  //prod/main/test/bin/           /auto/tools/bin sync_module
sync_tools              pv      db.rev  //prod/main/tools/ddfab/        /auto/tools/bin sync_module
sync_tools              pv      db.rev  //prod/main/tools/ddr_dist/     /auto/tools/bin sync_module
sync_tools              pv      db.rev  //tools/main/p4_tools/          /auto/tools/bin sync_module
sync_tools              pv      db.rev  //sweb/doc.php                  /auto/tools/bin sync_module
#
sync_iweb               pv      db.rev  //iweb/                         /a/web/docs     sync_module
sync_iweb               pv      db.rev  //sweb/                         /a/web/docs     sync_module
sync_iweb               pv      db.rev  //prod/main/doc/pdf/            /a/web/docs     sync_module
sync_iweb               pv      db.rev  //prod/main/app/ddr/help/       /a/web/docs     sync_module
#
sync_support            pv      db.rev  //sweb/                           -             sync_module
sync_support            pv      db.rev  //tools/main/p4_tools/asup_index  -             sync_module
#
upd_fixes               rv      db.change       -                         -             upd_fixes
#
ddfab_*
EOA
}
#ddfab_app               pv      db.rev  //prod/main/app                 -               ddfab_module
#ddfab_os                pv      db.rev  //prod/main/os                  -               ddfab_module
#ddfab_app               pv      db.rev  //prod/p1.cifs/app              -               ddfab_module
#ddfab_os                pv      db.rev  //prod/p1.cifs/os               -               ddfab_module
#ddfab_release           pv      db.rev  //prod/beta7                    -               ddfab_module
#ddfab_release           pv      db.rev  //prod/1.0.7                    -               ddfab_module
my $Use_branch_actions = 0;
#  load the Actions table:
#  
#
sub loadactions
{
  if (! $Actions_file)
    { &def_actions(); }
  else
    {
      if (! open(A, "<$Actions_file"))
        {
          print STDERR "$Myname: can't open \"$Actions_file\": $!\n";
          exit 1;
        }
      while (<A>)
        { chomp; push(@rawActions, $_); }
      close A;
    }
  foreach my $action (@rawActions)
    {
      if ($action =~ /^ddfab_\*/)
        {
          $Use_branch_actions = 1;
          &load_branch_actions();
        }
      else
        {
          push (@Actions, $action);
          &log("loadactions(): $action");
        }
    }
}
sub load_branch_actions
{
  if (! open(BR, "$P4 branches |"))
    { print "$Myname: can't open \"$P4 branches\": $!\n"; exit 1; }
  @Branch_Actions = ();
  while (<BR>)
    {
      if (/^Branch ([^\s]+) [0-9\/]+ '\*([^;]+);/)
        {
          my $branch = $1;
          my $attrs = $2;
          if ($attrs =~ /\Winactive\W/) { next; }
          foreach my $attr (split(/\s+/, $attrs))
            {
              if ($attr eq "inactive") { last; }
              if ($attr =~ /^build(:.*)?$/)
                {
                  my $bld_type = $1;
                  $bld_type =~ s/^://;
                  if ($bld_type eq "daily") { next; }
		  if ($bld_type eq "") { $bld_type = $branch; }
                  $ent = "ddfab_$bld_type\tpv\tdb.rev\t//prod/$branch";
                  if ($bld_type eq "app" || $bld_type eq "os") { $ent .= "/$bld_type"; }
                  $ent .= "\t-\tddfab_module";
                  push(@Branch_Actions, $ent);
                  &log("load_branch_actions(): $ent");
                }
            }
        }
    }
  close (BR);
}
sub daemonize # courtesy of "man perlipc":
{
  chdir '/'               or die "Can't chdir to /: $!";
  open STDIN, '/dev/null' or die "Can't read /dev/null: $!";
  open STDOUT, '>/dev/null'
                          or die "Can't write to /dev/null: $!";
  defined(my $pid = fork) or die "Can't fork: $!";
  exit if $pid;
  setsid                  or die "Can't start a new session: $!";
  open STDERR, '>&STDOUT' or die "Can't dup stdout: $!";
}
# TBD: configurability & async signalling
use Carp;
use strict;
use Fcntl ':flock'; # import LOCK_* constants
$| = 1;
my $Myname;
($Myname = $0) =~ s%^.*/%%;
my $Usage = <<LIT;
$Myname: usage: $Myname [-a <actions-file>] [-J <journal>]
LIT
sub usage
{
  print STDERR $Usage;
  exit 1;
}
sub help
{
  print STDERR <<LIT;
$Usage
$Myname is an embryonic daemon that watches the tail of the Perforce
journal, and triggers actions when certain activities are seen there.
LIT
  exit 1;
}
# option switch variables get defaults here...
my @Args;
my $Args;
my $LOGFILE = "/a/web/docs/logs/$Myname";
my $Journal = "/a/share/p4root/journal";
my $Ddfab_dir = "/auto/tools/bin";
# DEBUG:
#$LOGFILE = "/a/home/rmg/src/p4jd/LOGLOG";
#$Journal = "FAKE";
while ($#ARGV >= 0)
  {
    if ($ARGV[0] eq "-J")
      {
        shift; if ($ARGV[0] < 0) { &usage; }
        $Journal = $ARGV[0]; shift; next;
      }
    elsif ($ARGV[0] eq "-L")
      {
        shift; if ($ARGV[0] < 0) { &usage; }
        $LOGFILE = $ARGV[0]; shift; next;
      }
    elsif ($ARGV[0] eq "-a")
      {
        shift; if ($ARGV[0] < 0) { &usage; }
        $Actions_file = $ARGV[0]; shift; next;
      }
    elsif ($ARGV[0] eq "-help")
      { &help; }
    elsif ($ARGV[0] =~ /^-/) { &usage; }
    if ($Args ne "") { $Args .= " "; }
    push(@Args, $ARGV[0]);
    shift;
  }
&log("$Myname: starting...\n");
&loadactions();
#  First, open the journal file
#
my $J_size;
#$J_size = 999999999; # DEBUG: fake a truncation
my $split_journal_inquot = 0;
my $split_journal_value;
my @j;
sub split_journal
{
  my ($l, $func) = @_;
  if (! $split_journal_inquot)
    {
      # We've got a new line, and we're not in quote, so reset the journal
      # field values array
      #
      @j = ();
    }
  while ($l)
    {
      if ($split_journal_inquot)
        {
          while ($l)
            {
              if ($l =~ /^@@(.*\n)/)
                {
                  $l = $1;
                  $split_journal_value .= "@";
                  next;
                }
              if ($l =~ /^@(.*\n)/)
                {
                  $l = $1;
                  push(@j, $split_journal_value);
                  $split_journal_inquot = 0;
                  last;
                }
              if ($l =~ /^([^@]*\n)/)
                {
                  $split_journal_value .= $1;
                  $l = "";
                  last;
                }
              if ($l =~ /^([^@]+)(@.*\n)/)
                {
                  $split_journal_value .= $1;
                  $l = $2;
                  next;
                }
            }
        }
      else
        {
          if ($l eq "\n") { last; }
          if ($l =~ /^(\s+)(.*\n)/) { $l = $2; next; }
          if ($l =~ /^@(.*\n)/)
            {
              $l = $1;
              $split_journal_inquot = 1;
              $split_journal_value = "";
            }
          else
            {
              $l =~ /([^\s]+)(.*\n)/;
              push(@j, $1);
              $l = $2;
            }
        }
    }  
  #  We've emptied the line, and we're not in a quoted field, so
  #  we've got the entire journal entry; process it.
  #
  if (! $split_journal_inquot)
    {
no strict 'refs';
      &$func(@j);
use strict 'refs';
    }
}
sub log
{
  my ($m) = @_;
  my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
  open LOG, ">>$LOGFILE" || die "Can't open \"$LOGFILE\" for append";
  my $ts = sprintf("%02d-%02d-%04d %02d:%02d:%02d",
             $mon+1, $mday, $year+1900, $hour, $min, $sec);
  my @m = split(/\n/, $m);
  foreach my $l (@m)
    {
      printf LOG "$ts: [$$] $l\n";
      $ts =~ s/./ /g;
    }
  close LOG
}
sub tail_check
{
  my ($func) =  @_;
  
  my @s = stat J;
  if ($#s < 0) { &log("fstat \"$Journal\" failed: $!\n"); exit 1; }      
  my $C_size = $s[7];
  if ($C_size == $J_size) { return; }
  if ($C_size < $J_size)
    {
      #  We were truncated; rewind:
      if (! seek(J, 0, 0))
        { &log("Could not rewind \"$Journal\": $!\n"); exit 1; }      
      if ($C_size == 0)
        {
          $J_size = 0;
          return 0;
        }
    }
  #  If here, either size grew, or we truncated and there's
  #  stuff to read.
  #
  if (! flock(J, LOCK_EX))
    { &log("couldn't flock LOCK_EX \"$Journal\": $!\n"); exit 1; }
  #  Just stash stuff in here, so we can promptly relinquish the lock.
  # 
  my @J;
  while (<J>) { push(@J, $_); }
  if (! flock(J, LOCK_UN))
    { &log("couldn't flock LOCK_UN \"$Journal\": $!\n"); exit 1; }
  #  Now process what we read...
  #
  foreach $_ (@J) { split_journal($_, $func); }
  @s = stat J;
  if ($#s < 0) { &log("fstat \"$Journal\" failed: $!\n"); exit 1; }      
  $J_size = $s[7];
  return 1;
}
sub tail
{
  my($Journal, $func) = @_;
  while (1)
    {
      if (open(J, "<$Journal")) { last; }
      if ($! =~ /^No such file or directory/i)
        {
          &log("Could not open journal \"$Journal\": $!\n");
          exit 1;
        }      
      sleep 1;
    }
  &log("opened $Journal\n");
  if (! seek(J, 0, 2))
    { &log("Could not seek to end of \"$Journal\": $!\n"); exit 1; }      
  my @s = stat J;
  if ($#s < 0) { &log("fstat \"$Journal\" failed: $!\n"); exit 1; }      
  $J_size = $s[7];
  while (1)
    {
      &tail_check($func);
      sleep 1;
      #  Recieve the souls of lost zombies...
      #
      waitpid(-1,&WNOHANG);
    }
}
my $have_change = 0;
sub action
{
  my ($name, $cmd) = @_;
  &log("$name> $cmd\n");
  my $cmdout = `$cmd`; my $sts = $?;
  my $schr = ($sts ? "!" : "=");
  &log("$name$schr $cmdout\n");
  return $sts;
}
sub waitfor
{
  my ($name, $change) = @_;
  # To be safe, wait until "p4 counter change" reflects this change.
  # We won't wait forever, however; if we don't see the expected
  # change within a reasonable time, we just give up.
  #
  my $i;
  for ($i = 20; $i; $i--)
     {
       my $cur_change = `$P4 counter change 2>&1`;
       chop $cur_change;
       if ($cur_change >= $change) { return 1; }
       if ($i > 10) { sleep 1; } else { sleep 3; }
    }
  &log("$name: *** timed out waiting for the change counter!\n");
  return 0;
}
my %Sync_last;
sub sync_module
{
  my($name, $dir, @j) = @_;
  my $change = $j[7];
  # Only one per customer at a given change level
  #
  if (! ($change > $Sync_last{$name})) { return; }
  # Having this up here means that we'll only try triggering for this
  # $mod/$change one time. But relying on subsequent records (if the
  # change contained multiple matching files, for instance) for
  # retries after failures isn't right. Our contract is to bump the
  # handler action once for each applicable change set.
  #
  $Sync_last{$name} = $change;
  my $have = `$P4 counter $name 2>&1`;
  if ($change > $have)
    {      
      &log("$name: $P4 sync -f \@$change,$change\n");
      my $pid;
      if (! ($pid = fork()))
        {
          # In the child
          if ($dir ne "-" && (! chdir $dir))
            {
              &log("name: *** could not cd to $dir: $!\n");
              exit 1;
            }
          if (&waitfor($name, $change))
            {
              my $sts;
              #  Look for sync_* for clients on other hosts...
	      #  (Yep, having this here is bush league... but for now...)
              #
              #  Incestuousness follows... :-)
              #
              #  Data Domain entries:		
              #
              if ($name eq "sync_tools_fs1")
                {
                  if (! ($sts = &action($name, "ssh fs1 /auto/tools/bin/syncit-fs1 $change")))
                    { &action($name, "$P4 counter $name $change 2>&1"); }
                  exit $sts;
                }
              if ($name eq "sync_tools_morgan_a")
                {
                  if (! ($sts = &action($name, "ssh morgan /a/tools/bin/syncit-morgan_a $change")))
                    { &action($name, "$P4 counter $name $change 2>&1"); }
                  exit $sts;
                }
              if ($name eq "sync_support")
                {
                  if (! ($sts = &action($name, "ssh support /var/www/html/syncit $change")))
                    { &action($name, "$P4 counter $name $change 2>&1"); }
                  exit $sts;
                }
              if ($name eq "sync_salesedge")
                {
                  if (! ($sts = &action($name, "ssh support /var/www/salesedge/syncit $change")))
                    { &action($name, "$P4 counter $name $change 2>&1"); }
                  exit $sts;
                }
              if ($name eq "sync_ddfab_confs")
                {
                  if (! ($sts = &action($name, "ssh build /auto/builds/syncit $change")))
                    { &action($name, "$P4 counter $name $change 2>&1"); }
                  exit $sts;
                }
              #  rmg personal entries:
              #
              if ($name eq "sync_foxcove_wheel")
                {
                  if (! ($sts = &action($name, "ssh -l rmg wheel.foxcove.com /usr/rmg/foxcove/syncit $change")))
                    { &action($name, "$P4 counter $name $change 2>&1"); }
                  exit $sts;
                }
              if ($name eq "sync_foxcove_chinacat")
                {
                  if (! ($sts = &action($name, "ssh -l rmg chinacat.foxcove.com /home/rmg/web/foxcove/syncit $change")))
                    { &action($name, "$P4 counter $name $change 2>&1"); }
                  exit $sts;
                }
              if ($name eq "sync_foxcove_touchofgrey")
                {
                  if (! ($sts = &action($name, "ssh -l rmg touchofgrey.foxcove.com /Users/rmg/Sites/foxcove/syncit $change")))
                    { &action($name, "$P4 counter $name $change 2>&1"); }
                  exit $sts;
                }
              # Note: we rely on the $P4CONFIG stuff here!
              #
              delete $ENV{"PWD"};
              delete $ENV{"USER"};
              delete $ENV{"USERNAME"};
              if (! ($sts = &action($name, "$P4 sync -f \@$change,$change 2>&1")))
                { &action($name, "$P4 counter $name $change 2>&1"); }
              exit $sts;
            }
          &log("$name: *** timed out waiting for the change counter update\n");
          exit 1;
        }
    }
}
my %Ddfab_last;
sub ddfab_module
{
  my($name, $dir, @j) = @_;
  my $change = $j[7];
  # Only one per customer at a given change level
  #
  if (! ($change > $Ddfab_last{$name})) { return; }
  # Having this up here means that we'll only try triggering for this
  # $mod/$change one time. But relying on subsequent records (if the
  # change contained multiple matching files, for instance) for
  # retries after failures isn't right. Our contract is to bump the
  # handler action once for applicable change set.
  #
  $Ddfab_last{$name} = $change;
  my $mod = $name;
  $mod =~ s/^.*_//;
  my $have = `$P4 counter $name 2>&1`;
  if ($change > $have)
    {      
      &log("$name: ddfab_run_$mod apache 0002\n");
      my $pid;
      if (! ($pid = fork()))
        {
          # In the child
          if (&waitfor($name, $change))
            {
              # For the build daemon stuff, we just bump the
              # ddfab_run_$mod script; _it_ will decide when to update
              # the counter.
              #
              my $branch = "";
              if ($mod !~ /^(app|os|dev|release|daily)$/) { $branch = "branch "; }
              my $sts = &action($name, "/usr/bin/rsh build $Ddfab_dir/ddfab_run $branch$mod apache 0002 2>&1");
              exit $sts;
            }
            
          &log("name: *** timed out waiting for the change counter update\n");
          exit 1;
        }
    }
}
sub handle_entry
{
  my (@j) = @_;
  my @Perform_Actions;
  #  Reload the branch actions if we see any branch spec changed
  #
  if ($Use_branch_actions)
    {
      if ($j[2] eq "db.domain" && $j[4] eq "98") { &load_branch_actions(); }
      @Perform_Actions = (@Actions, @Branch_Actions);
    }
  else
    { @Perform_Actions = @Actions; }
  foreach my $a (@Perform_Actions)
    {
      if ($a =~ /^\s*\#/ || $a =~ /^\s*$/) { next; }
      my($name, $op, $table, $path, $chdir, $action) = split(/\s+/, $a);
      #  The check for !~ $path/(test|doc)/ is a bit of a kludge, but
      #  makes life easier for the doc & testfolk... 
      #
      if ($j[0] eq $op && $j[2] eq $table && $j[3] =~ /$path/ && $j[3] !~ /\/\/prod\/[^\/]+\/(test|doc)\//)
        {
          no strict 'refs';
          &$action($name, $chdir, @j);
          use strict 'refs';
        }
    }
}
daemonize;
&tail($Journal, "handle_entry");
                    | # | Change | User | Description | Committed | |
|---|---|---|---|---|---|
| #55 | 5102 | Richard Geiger | Life is full of special cases... | ||
| #54 | 4974 | Richard Geiger | 
                    mainly comments and one simple code structure cleanup.  | 
                ||
| #53 | 4882 | Richard Geiger | 
                    Typo - grrr. Monday Monday.  | 
                ||
| #52 | 4881 | Richard Geiger | Use the per-host syncit scripts. | ||
| #51 | 4880 | Richard Geiger | 
                    Musical hosts. (like musical chairs).  | 
                ||
| #50 | 4828 | Richard Geiger | Will I never learn? | ||
| #49 | 4827 | Richard Geiger | This is getting Silly. | ||
| #48 | 4628 | Richard Geiger | 
                    Handle sync_salesedge correctly. reviewer: wade  | 
                ||
| #47 | 4289 | Richard Geiger | Don't load actions for inactive branches. | ||
| #46 | 4288 | Richard Geiger | 
                    Oops. Need the extra "branch" keyword to ddfab_run for per-branch builds.  | 
                ||
| #45 | 4287 | Richard Geiger | Handle the new "build" (per-branch builds) type. | ||
| #44 | 4285 | Richard Geiger | Add a comment. | ||
| #43 | 4192 | Richard Geiger | Exempt prod/<branch>/doc from triggering automatic builds. | ||
| #42 | 4013 | Richard Geiger | Adjust config for asup_index. | ||
| #41 | 3935 | Richard Geiger | Pass the entire journal entry to the handlers. | ||
| #40 | 3934 | Richard Geiger | Tweak to fix the auto-branch actions load stuff. | ||
| #39 | 3933 | Richard Geiger | A tweak, so that the log will show the branch_action_reloads. | ||
| #38 | 3932 | Richard Geiger | 
                    Mainly, this change makes p4jd more dynamically configurable when a "ddfab_*" entry is in effect: in this case, the config entries derived from the branches info is regenerated whenever any branch domain record is seen.  | 
                ||
| #37 | 3915 | Richard Geiger | Pass the change number to the remote "syncit" command. | ||
| #36 | 3914 | Richard Geiger | 
                    Only sync the files from the changelist being done. (Need this now, with the advent of -f, or the whole blooming site get's refreshed!).  | 
                ||
| #35 | 3913 | Richard Geiger | 
                    Use -f for auto-syncs (in case there's an old pre-Perfortification version already in the live tree).  | 
                ||
| #34 | 3699 | Richard Geiger | 
                    Immunize all //prod/<branch>/test/ paths against auto build tiggering.  | 
                ||
| #33 | 3696 | Richard Geiger | Spiff up the log entries, and add s "starting" message. | ||
| #32 | 3695 | Richard Geiger | Log the configuration loaded. | ||
| #31 | 3694 | Richard Geiger | 
                    These changes finally decouple the p4jd configuration from needing to be hardwired in the script. It can now get its configuration via a combination of a static configuration file plus the contents of Perforce branch specs. Once less edit to make when adding a new branch (but we still have to remember to at least restart p4jd!)  | 
                ||
| #30 | 3664 | Richard Geiger | enable 1.0.7 builds. | ||
| #29 | 3657 | Richard Geiger | Use the new grp * umask parms to ddfab_run | ||
| #28 | 3653 | Richard Geiger | 
                    We keep the latest //sweb/doc.php here, for use by the multiboot Makefile.  | 
                ||
| #27 | 3627 | Richard Geiger | Do p1.cifs | ||
| #26 | 3617 | Richard Geiger | beta6 -> beta7 | ||
| #25 | 3555 | Richard Geiger | progress... | ||
| #24 | 3504 | Richard Geiger | 
                    Always use -u p4. (Allows us to lock the live workspaces)  | 
                ||
| #23 | 3461 | Richard Geiger | ddr_dist now lives under //prod/<branch>/tools/, like ddfab. | ||
| #22 | 3435 | Richard Geiger | beta5 tweakage. | ||
| #21 | 3394 | Richard Geiger | Fix ssh args for sync_support. | ||
| #20 | 3357 | Richard Geiger | Set up for beta4 auto-builds. | ||
| #19 | 3313 | Richard Geiger | 
                    Config for trigger a sync_module on the support server, via ssh.  | 
                ||
| #18 | 3304 | Richard Geiger | 
                    Add auto-sync of //prod/main/test/bin/ Switch release branch to beta3.  | 
                ||
| #17 | 3303 | Richard Geiger | 
                    Tweak the config table, which really should be a spearate file, and not controlled on the Public Depot!  | 
                ||
| #16 | 3198 | Richard Geiger | 
                    Reconfig... (and whitespace)  | 
                ||
| #15 | 3195 | Richard Geiger | reconfig | ||
| #14 | 3179 | Richard Geiger | This config info really should be in a separate file! | ||
| #13 | 3175 | Richard Geiger | 
                    Just some config changes. These really should be in a separate config file!  | 
                ||
| #12 | 3058 | Richard Geiger | 
                    Tweak to how ddfab_run... is invoked.  | 
                ||
| #11 | 3056 | Richard Geiger | Update the general comments at the top and in the help message. | ||
| #10 | 3055 | Richard Geiger | 
                    A change intended to free the souls of lost zombies. I'm checking this in with no further testing than a perl syntax check. Am I damned? Will I myself be zombied for offenses such as this? And... what about Naomi?  | 
                ||
| #9 | 3024 | Richard Geiger | Add -a <actions> | ||
| #8 | 3003 | Richard Geiger | Fixes to the mult-ent support. | ||
| #7 | 3000 | Richard Geiger | 
                    Really only do a trigger only once per change, even if there are multiple matching revisions in the change.  | 
                ||
| #6 | 2997 | Richard Geiger | 
                    Clean up ddfab_* handling so we don't get multiple triggers form the same change (as when multiple files in the changes are from the module)  | 
                ||
| #5 | 2996 | Richard Geiger | Let ddfab_run_*'s do the ddfab_* counter updates. | ||
| #4 | 2995 | Richard Geiger | 
                    Teach it to be configurable so that it can monitor for N events. Slick, eh?  | 
                ||
| #3 | 2765 | Richard Geiger | 
                    log timeouts when waiting for the change counter to update. My first submit with p4v!  | 
                ||
| #2 | 2762 | Richard Geiger | daemonize it. | ||
| #1 | 2761 | Richard Geiger | initial p4jd submit. |