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";