3 # A daemon that waits for update events sent by its companion
 
   4 # post-receive-cinotify hook, checks out a new copy of source,
 
   5 # compiles it, and emails the guilty parties if the compile
 
   6 # (and optionally test suite) fails.
 
   8 # To use this daemon, configure it and run it.  It will disconnect
 
   9 # from your terminal and fork into the background.  The daemon must
 
  10 # have local filesystem access to the source repositories, as it
 
  11 # uses objects/info/alternates to avoid copying objects.
 
  13 # Add its companion post-receive-cinotify hook as the post-receive
 
  14 # hook to each repository that the daemon should monitor.  Yes, a
 
  15 # single daemon can monitor more than one repository.
 
  17 # To use multiple daemons on the same system, give them each a
 
  18 # unique queue file and tmpdir.
 
  22 # Reads from a Git style configuration file.  This will be
 
  23 # ~/.gitconfig by default but can be overridden by setting
 
  24 # the GIT_CONFIG_FILE environment variable before starting.
 
  27 #   Hostname of the SMTP server the daemon will send email
 
  28 #   through.  Defaults to 'localhost'.
 
  31 #   Username to authenticate to the SMTP server as.  This
 
  32 #   variable is optional; if it is not supplied then no
 
  33 #   authentication will be performed.
 
  35 # cidaemon.smtpPassword
 
  36 #   Password to authenticate to the SMTP server as.  This
 
  37 #   variable is optional.  If not supplied but smtpUser was,
 
  38 #   the daemon prompts for the password before forking into
 
  42 #   Type of authentication to perform with the SMTP server.
 
  43 #   If set to 'login' and smtpUser was defined, this will
 
  44 #   use the AUTH LOGIN command, which is suitable for use
 
  45 #   with at least one version of Microsoft Exchange Server.
 
  46 #   If not set the daemon will use whatever auth methods
 
  47 #   are supported by your version of Net::SMTP.
 
  50 #   Email address that daemon generated emails will be sent
 
  51 #   from.  This should be a useful email address within your
 
  52 #   organization.  Required.
 
  55 #   Human friendly name that the daemon will send emails as.
 
  56 #   Defaults to 'cidaemon'.
 
  59 #   Number of seconds to sleep between polls of the queue file.
 
  62 # cidaemon.recentCache
 
  63 #   Number of recent commit SHA-1s per repository to cache and
 
  64 #   skip building if they appear again.  This is useful to avoid
 
  65 #   rebuilding the same commit multiple times just because it was
 
  66 #   pushed into more than one branch.  Defaults to 100.
 
  69 #   Scratch directory to create the builds within.  The daemon
 
  70 #   makes a new subdirectory for each build, then deletes it when
 
  71 #   the build has finished.  The pid file is also placed here.
 
  75 #   Path to the queue file that the post-receive-cinotify hook
 
  76 #   appends events to.  This file is polled by the daemon.  It
 
  77 #   must not be on an NFS mount (uses flock).  Required.
 
  80 #   Perl regex patterns to match against author and committer
 
  81 #   lines.  If a pattern matches, that author or committer will
 
  82 #   not be notified of a build failure.
 
  84 # Per Repository Config
 
  85 # ----------------------
 
  86 # Read from the source repository's config file.
 
  89 #   Shell command to execute the build.  This command must
 
  90 #   return 0 on "success" and non-zero on failure.  If you
 
  91 #   also want to run a test suite, make sure your command
 
  92 #   does that too.  Required.
 
  95 #   Queue file to notify the cidaemon through.  Should match
 
  96 #   cidaemon.queue.  If not set the hook will not notify the
 
 100 #   Perl regex patterns of refs that should not be sent to
 
 101 #   cidaemon.  Updates of these refs will be ignored.
 
 103 # builder.newBranchBase
 
 104 #   Glob patterns of refs that should be used to form the
 
 105 #   'old' revions of a newly created ref.  This should set
 
 106 #   to be globs that match your 'mainline' branches.  This
 
 107 #   way a build failure of a brand new topic branch does not
 
 108 #   attempt to email everyone since the beginning of time;
 
 109 #   instead it only emails those authors of commits not in
 
 110 #   these 'mainline' branches.
 
 112 local $ENV{PATH} = join ':', qw(
 
 120 use FindBin qw($RealBin);
 
 122 use lib File::Spec->catfile($RealBin, '..', 'perl5');
 
 123 use Storable qw(retrieve nstore);
 
 125 use POSIX qw(strftime);
 
 126 use Getopt::Long qw(:config no_auto_abbrev auto_help);
 
 131         my $required = shift || 0;
 
 133         open GIT, '-|','git','config','--get',$var;
 
 137         die "error: $var not set.\n" if ($required && !$r);
 
 141 package EXCHANGE_NET_SMTP;
 
 143 # Microsoft Exchange Server requires an 'AUTH LOGIN'
 
 144 # style of authentication.  This is different from
 
 145 # the default supported by Net::SMTP so we subclass
 
 146 # and override the auth method to support that.
 
 150 use MIME::Base64 qw(encode_base64);
 
 151 our @ISA = qw(Net::SMTP);
 
 152 our $auth_type = ::git_config 'cidaemon.smtpAuth';
 
 157         my $type = ref($self) || $self;
 
 158         $type->SUPER::new(@_);
 
 164         return $self->SUPER::auth(@_) unless $auth_type eq 'login';
 
 166         my $user = encode_base64 shift, '';
 
 167         my $pass = encode_base64 shift, '';
 
 168         return 0 unless CMD_MORE == $self->command("AUTH LOGIN")->response;
 
 169         return 0 unless CMD_MORE == $self->command($user)->response;
 
 170         CMD_OK == $self->command($pass)->response;
 
 175 my ($debug_flag, %recent);
 
 177 my $ex_host = git_config('cidaemon.smtpHost') || 'localhost';
 
 178 my $ex_user = git_config('cidaemon.smtpUser');
 
 179 my $ex_pass = git_config('cidaemon.smtpPassword');
 
 181 my $ex_from_addr = git_config('cidaemon.email', 1);
 
 182 my $ex_from_name = git_config('cidaemon.name') || 'cidaemon';
 
 184 my $scan_delay = git_config('cidaemon.scanDelay') || 60;
 
 185 my $recent_size = git_config('cidaemon.recentCache') || 100;
 
 186 my $tmpdir = git_config('cidaemon.tmpdir') || '/tmp';
 
 187 my $queue_name = git_config('cidaemon.queue', 1);
 
 188 my $queue_lock = "$queue_name.lock";
 
 191 open GIT,'git config --get-all cidaemon.nocc|';
 
 201         foreach my $pat (@nocc_list) {
 
 222         my $end = sub {system('stty','echo');print "\n";exit};
 
 223         local $SIG{TERM} = $end;
 
 224         local $SIG{INT} = $end;
 
 225         system('stty','-echo');
 
 230         system('stty','echo');
 
 238          strftime("%a, %d %b %Y %H:%M:%S %Z", localtime);
 
 243         my ($subj, $body, $to) = @_;
 
 244         my $now = rfc2822_date;
 
 251                 $to_str .= ', ' if $to_str;
 
 253                 push @rcpt_to, $1 if $s =~ /<(.*)>/;
 
 255         die "Nobody to send to.\n" unless @rcpt_to;
 
 257 From: "$ex_from_name" <$ex_from_addr>
 
 265         my $smtp = EXCHANGE_NET_SMTP->new(Host => $ex_host)
 
 266                 or die "Cannot connect to $ex_host: $!\n";
 
 267         if ($ex_user && $ex_pass) {
 
 268                 $smtp->auth($ex_user,$ex_pass)
 
 269                         or die "$ex_host rejected $ex_user\n";
 
 271         $smtp->mail($ex_from_addr)
 
 272                 or die "$ex_host rejected $ex_from_addr\n";
 
 273         scalar($smtp->recipient(@rcpt_to, { SkipBad => 1 }))
 
 274                 or die "$ex_host did not accept any addresses.\n";
 
 276                 or die "$ex_host rejected message data\n";
 
 282         open LOCK, ">$queue_lock" or die "Can't open $queue_lock: $!";
 
 285         my $queue = -f $queue_name ? retrieve $queue_name : [];
 
 286         my $ent = shift @$queue;
 
 287         nstore $queue, $queue_name;
 
 296         system('git',@_) == 0 or die "Cannot git " . join(' ', @_) . "\n";
 
 301         open(C, '-|','git',@_);
 
 310         my ($git_dir, $new) = @_;
 
 312         my $tmp = File::Spec->catfile($tmpdir, "builder$$");
 
 313         system('rm','-rf',$tmp) == 0 or die "Cannot clear $tmp\n";
 
 314         die "Cannot clear $tmp.\n" if -e $tmp;
 
 320                         local $ENV{GIT_DIR} = $git_dir;
 
 321                         $command = git_val 'config','builder.command';
 
 323                 die "No builder.command for $git_dir.\n" unless $command;
 
 325                 git_exec 'clone','-n','-l','-s',$git_dir,$tmp;
 
 326                 chmod 0700, $tmp or die "Cannot lock $tmp\n";
 
 327                 chdir $tmp or die "Cannot enter $tmp\n";
 
 329                 git_exec 'update-ref','HEAD',$new;
 
 330                 git_exec 'read-tree','-m','-u','HEAD','HEAD';
 
 333                         print STDERR "failed to execute '$command': $!\n";
 
 337                         print STDERR "'$command' died from signal $sig\n";
 
 341                         print STDERR "'$command' exited with $r\n" if $r;
 
 351         system('rm','-rf',$tmp);
 
 356 sub build_failed ($$$$$)
 
 358         my ($git_dir, $ref, $old, $new, $msg) = @_;
 
 360         $git_dir =~ m,/([^/]+)$,;
 
 362         $ref =~ s,^refs/(heads|tags)/,,;
 
 368                 local $ENV{GIT_DIR} = $git_dir;
 
 370                 push @revs, '--not', @$old if @$old;
 
 371                 open LOG,'-|','git','rev-list','--pretty=raw',@revs;
 
 373                         if (s/^(author|committer) //) {
 
 376                                 $authors{$_} = 1 unless nocc_author $_;
 
 380                 open LOG,'-|','git','shortlog',@revs;
 
 381                 $shortlog .= $_ while <LOG>;
 
 383                 $revstr = join(' ', @revs);
 
 386         my @to = sort keys %authors;
 
 388                 print STDERR "error: No authors in $revstr\n";
 
 392         my $subject = "[$repo_name] $ref : Build Failed";
 
 400 --------------------------------------------------------------
 
 403         send_email($subject, $body, \@to);
 
 408         my ($git_dir, $ref, $old, $new) = @_;
 
 412                 push @revs, '--not', @$old if @$old;
 
 413                 print "BUILDING $git_dir\n";
 
 414                 print "  BRANCH: $ref\n";
 
 415                 print "  COMMITS: ", join(' ', @revs), "\n";
 
 419         pipe R, W or die "cannot pipe builder: $!";
 
 421         my $builder = fork();
 
 422         if (!defined $builder) {
 
 423                 die "cannot fork builder: $!";
 
 424         } elsif (0 == $builder) {
 
 426                 close STDIN;open(STDIN, '/dev/null');
 
 429                 exit do_build $git_dir, $new;
 
 433                 $out .= $_ while <R>;
 
 436                 build_failed $git_dir, $ref, $old, $new, $out if $?;
 
 439         print "DONE\n\n" if $debug_flag;
 
 445         my $stop_sub = sub {$run = 0};
 
 446         $SIG{HUP} = $stop_sub;
 
 447         $SIG{INT} = $stop_sub;
 
 448         $SIG{TERM} = $stop_sub;
 
 451         my $pidfile = File::Spec->catfile($tmpdir, "cidaemon.pid");
 
 452         open(O, ">$pidfile"); print O "$$\n"; close O;
 
 457                         my ($git_dir, $ref, $old, $new) = @$ent;
 
 459                         $ent = $recent{$git_dir};
 
 460                         $recent{$git_dir} = $ent = [[], {}] unless $ent;
 
 461                         my ($rec_arr, $rec_hash) = @$ent;
 
 462                         next if $rec_hash->{$new}++;
 
 463                         while (@$rec_arr >= $recent_size) {
 
 464                                 my $to_kill = shift @$rec_arr;
 
 465                                 delete $rec_hash->{$to_kill};
 
 467                         push @$rec_arr, $new;
 
 469                         run_build $git_dir, $ref, $old, $new;
 
 480         'debug|d' => \$debug_flag,
 
 481         'smtp-user=s' => \$ex_user,
 
 482 ) or die "usage: $0 [--debug] [--smtp-user=user]\n";
 
 484 $ex_pass = input_noecho("$ex_user SMTP password: ")
 
 485         if ($ex_user && !$ex_pass);
 
 493 if (!defined $daemon) {
 
 494         die "cannot fork daemon: $!";
 
 495 } elsif (0 == $daemon) {
 
 496         close STDIN;open(STDIN, '/dev/null');
 
 497         close STDOUT;open(STDOUT, '>/dev/null');
 
 498         close STDERR;open(STDERR, '>/dev/null');
 
 502         print "Daemon $daemon running in the background.\n";