diff options
Diffstat (limited to 'lib/Buildd/Mail.pm')
-rw-r--r-- | lib/Buildd/Mail.pm | 1354 |
1 files changed, 1354 insertions, 0 deletions
diff --git a/lib/Buildd/Mail.pm b/lib/Buildd/Mail.pm new file mode 100644 index 0000000..09f43ec --- /dev/null +++ b/lib/Buildd/Mail.pm @@ -0,0 +1,1354 @@ +# buildd-mail: mail answer processor for buildd +# Copyright © 1998 Roman Hodek <Roman.Hodek@informatik.uni-erlangen.de> +# Copyright © 2009 Roger Leigh <rleigh@debian.org> +# Copyright © 2005 Ryan Murray <rmurray@debian.org> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see +# <http://www.gnu.org/licenses/>. +# +####################################################################### + +package Buildd::Mail; + +use strict; +use warnings; + +use Buildd qw(ll_send_mail lock_file unlock_file send_mail exitstatus); +use Buildd::Conf qw(); +use Buildd::Base; +use Sbuild qw(binNMU_version $devnull); +use Sbuild::ChrootRoot; +use Buildd::Client; +use POSIX; +use File::Basename; +use MIME::QuotedPrint; +use MIME::Base64; +use Encode; + +BEGIN { + use Exporter (); + our (@ISA, @EXPORT); + + @ISA = qw(Exporter Buildd::Base); + + @EXPORT = qw(); +} + +sub new { + my $class = shift; + my $conf = shift; + + my $self = $class->SUPER::new($conf); + bless($self, $class); + + $self->set('Mail Error', undef); + $self->set('Mail Short Error', undef); + $self->set('Mail Header', {}); + $self->set('Mail Body Text', ''); + + $self->open_log(); + + return $self; +} + +sub run { + my $self = shift; + + chdir($self->get_conf('HOME')); + + $self->set('Mail Error', undef); + $self->set('Mail Short Error', undef); + $self->set('Mail Header', {}); + $self->set('Mail Body Text', ''); + + $self->process_mail(); + + return 0; +} + + +sub process_mail () { + my $self = shift; + +# Note: Mail Header (to|from|subject|message-id|date) are mandatory. +# Check for these and bail out if not present. + my $header_text = ""; + my $lastheader = ""; + + $self->set('Mail Header', {}); + + $self->set('Mail Error', ''); + $self->set('Mail Short Error', ''); + $self->set('Mail Header', {}); + $self->set('Mail Body Text', ''); + + while( <STDIN> ) { + $header_text .= $_; + last if /^$/; + + if (/^\s/ && $lastheader) { + $_ =~ s/^\s+//; + $_ = "$lastheader $_"; + } + + if (/^From (\S+)/) { + ; + } + if (/^([\w\d-]+):\s*(.*)\s*$/) { + my $hname; + ($hname = $1) =~ y/A-Z/a-z/; + $self->get('Mail Header')->{$hname} = $2; + $lastheader = $_; + chomp( $lastheader ); + } + else { + $lastheader = ""; + } + } + while( <STDIN> ) { + last if !/^\s*$/; + } + + $self->set('Mail Body Text', + $self->get('Mail Body Text') . $_) + if defined($_); + + if (!eof) + { + local($/); + undef $/; + $self->set('Mail Body Text', + $self->get('Mail Body Text') . <STDIN>); + } + + if ($self->get('Mail Header')->{'from'} =~ /mail\s+delivery\s+(sub)?system|mailer.\s*daemon/i) { + # is an error mail from a mailer daemon + # To avoid mail loops if this error resulted from a mail we sent + # outselves, we break the loop by not forwarding this mail after the 5th + # error mail within 8 hours or so. + my $n = $self->add_error_mail(); + if ($n > 5) { + $self->log("Too much error mails ($n) within ", + int($self->get_conf('ERROR_MAIL_WINDOW')/(60*60)), " hours\n", + "Not forwarding mail from ".$self->get('Mail Header')->{'from'}."\n", + "Subject: " . $self->get('Mail Header')->{'subject'} . "\n"); + return; + } + } + + goto forward_mail if !$self->get('Mail Header')->{'subject'}; + my $subject = $self->get('Mail Header')->{'subject'}; + Encode::from_to($subject, "MIME-Header", "utf-8"); + + if ($subject =~ /^Re: Log for \S+ build of (\S+)(?: on [\w-]+)? \(dist=(\S+)\)/i) { + # reply to a build log + my( $package, $dist_name ) = ( $1, $2 ); + + my $dist_config = $self->get_dist_config_by_name($dist_name); + return if (!$dist_config); #get_dist_config sets the error mail + + my $text = $self->get('Mail Body Text'); + $text =~ /^(\S+)/; + $self->set('Mail Body Text', $text); + if (defined($self->get('Mail Header')->{'content-transfer-encoding'})) { + # Decode the mail if necessary. + if ($self->get('Mail Header')->{'content-transfer-encoding'} =~ /quoted-printable/) { + $self->set('Mail Body Text', + decode_qp($self->get('Mail Body Text'))); + } elsif ($self->get('Mail Header')->{'content-transfer-encoding'} =~ /base64/) { + $self->set('Mail Body Text', + decode_base64($self->get('Mail Body Text'))); + } + } + my $keyword = $1; + my $from = $self->get('Mail Header')->{'from'}; + $from = $1 if $from =~ /<(.+)>/; + $self->log("Log reply from $from\n"); + my %newv; + + if ($keyword =~ /^not-for-us/) { + $self->no_build( $package, $dist_config ); + $self->purge_pkg( $package, $dist_config ); + } + elsif ($keyword =~ /^up(l(oad)?)?-rem/) { + $self->remove_from_upload( $package, $dist_config ); + } + elsif ($self->check_is_outdated( $dist_config, $package )) { + # Error has been set already -> no action here + } + elsif ($keyword =~ /^fail/) { + my $text = $self->get('Mail Body Text'); + $text =~ s/^fail.*\n(\s*\n)*//; + $text =~ s/\n+$/\n/; + $self->set_to_failed( $package, $dist_config, $text ); + $self->purge_pkg( $package, $dist_config ); + } + elsif ($keyword =~ /^ret/) { + if (!$self->check_state( $package, $dist_config, qw(Built Building Build-Attempted) )) { + # Error already set + } + else { + $self->append_to_REDO( $package, $dist_config ); + } + } + elsif ($keyword =~ /^d(ep(endency)?)?-(ret|w)/) { + if (!$self->check_state( $package, $dist_config, qw(Built Building Build-Attempted) )) { + # Error already set + } + else { + $self->get('Mail Body Text') =~ /^\S+\s+(.*)$/m; + my $deps = $1; + $self->set_to_depwait( $package, $dist_config, $deps ); + $self->purge_pkg( $package, $dist_config ); + } + } + elsif ($keyword =~ /^man/) { + if (!$self->check_state( $package, $dist_config, qw(Built Building Build-Attempted) )) { + # Error already set + } + else { + # no action + $self->log("$package($dist_name) will be finished manually\n"); + } + } + elsif ($keyword =~ /^newv/) { + # build a newer version instead + $self->get('Mail Body Text') =~ /^newv\S*\s+(\S+)/; + my $newv = $1; + if ($newv =~ /_/) { + $self->log("Removing unneeded package name from $newv\n"); + $newv =~ s/^.*_//; + $self->log("Result: $newv\n"); + } + my $pkgname; + ($pkgname = $package) =~ s/_.*$//; + $self->redo_new_version( $dist_config, $package, "${pkgname}_${newv}" ); + $self->purge_pkg( $package, $dist_config ); + } + elsif ($keyword =~ /^(give|back)/) { + $self->get('Mail Body Text') =~ /^(give|back) ([-0-9]+)/; + my $pri = $1; + if (!$self->check_state( $package, $dist_config, qw(Built Building Build-Attempted) )) { + # Error already set + } + else { + $self->give_back( $package, $dist_config ); + $self->purge_pkg( $package, $dist_config ); + } + } + elsif ($keyword =~ /^purge/) { + $self->purge_pkg( $package, $dist_config ); + } + elsif ($self->get('Mail Body Text') =~ /^---+\s*BEGIN PGP SIGNED MESSAGE/) { + if ($self->prepare_for_upload( $package, + $self->get('Mail Body Text') )) { + $self->purge_pkg( $package, $dist_config ); + } + } + elsif ($self->get('Mail Body Text') =~ /^--/ && + $self->get('Mail Header')->{'content-type'} =~ m,multipart/signed,) { + my ($prot) = ($self->get('Mail Header')->{'content-type'} =~ m,protocol="([^"]*)",); + my ($bound) = ($self->get('Mail Header')->{'content-type'} =~ m,boundary="([^"]*)",); + my $text = $self->get('Mail Body Text'); + $text =~ s,^--\Q$bound\E\nContent-Type: text/plain; charset=us-ascii\n\n,-----BEGIN PGP SIGNED MESSAGE-----\n\n,; + $text =~ s,--\Q$bound\E\nContent-Type: application/pgp-signature\n\n,,; + $text =~ s,\n\n--\Q$bound\E--\n,,; + $self->set('Mail Body Text', $text); + if ($self->prepare_for_upload($package, + $self->get('Mail Body Text'))) { + $self->purge_pkg( $package, $dist_config ); + } + } + else { + $self->set('Mail Short Error', + $self->get('Mail Short Error') . + "Bad keyword in answer $keyword\n"); + $self->set('Mail Error', + $self->get('Mail Error') . + "Answer not understood (expected retry, failed, manual,\n". + "dep-wait, giveback, not-for-us, purge, upload-rem,\n". + "newvers, or a signed changes file)\n"); + } + } + elsif ($subject =~ /^Re: Should I build (\S+) \(dist=(\S+)\)/i) { + # reply whether a prev-failed package should be built + my( $package, $dist_name ) = ( $1, $2 ); + + my $dist_config = $self->get_dist_config_by_name($dist_name); + return if (!$dist_config); #get_dist_config sets the error mail + + $self->get('Mail Body Text') =~ /^(\S+)/; + my $keyword = $1; + $self->log("Should-build reply for $package($dist_name)\n"); + if ($self->check_is_outdated( $dist_config, $package )) { + # Error has been set already -> no action here + } + elsif (!$self->check_state( $package, $dist_config, qw(Built Building Build-Attempted) )) { + # Error already set + } + elsif ($keyword =~ /^(build|ok)/) { + $self->append_to_REDO( $package, $dist_config ); + } + elsif ($keyword =~ /^fail/) { + my $text = $self->get_fail_msg( $package, $dist_config ); + $self->set_to_failed( $package, $dist_config, $text ); + } + elsif ($keyword =~ /^(not|no-b)/) { + $self->no_build( $package, $dist_config ); + } + elsif ($keyword =~ /^(give|back)/) { + $self->give_back( $package, $dist_config ); + } + else { + $self->set('Mail Short Error', + $self->get('Mail Short Error') . + "Bad keyword in answer $keyword\n"); + $self->set('Mail Error', + $self->get('Mail Error') . + "Answer not understood (expected build, ok, fail, ". + "give-back, or no-build)\n"); + } + } + elsif ($subject =~ /^Processing of (\S+)/) { + my $job = $1; + # mail from Erlangen queue daemon: forward all non-success messages + my $text = $self->get('Mail Body Text'); + goto forward_mail if $text !~ /uploaded successfully/mi; + $self->log("$job processed by upload queue\n") + if $self->get_conf('LOG_QUEUED_MESSAGES'); + } + elsif ($subject =~ /^([-+~\.\w]+\.changes) (INSTALL|ACCEPT)ED/) { + # success mail from dinstall + my $changes_f = $1; + my( @to_remove, $upload_f, $pkgv ); + my @upload_dirs = $self->find_upload_dirs_for_changes_file($changes_f); + + if ((scalar @upload_dirs) < 1) { + $self->log("Can't identify upload directory for $changes_f!\n"); + return 0; + } elsif ((scalar @upload_dirs) > 1) { + $self->log("Found more than one upload directory for $changes_f - not deleting binaries!\n"); + return 0; + } + my $upload_dir = $upload_dirs[0]; + + if (-f "$upload_dir/$changes_f" && open( F, "<$upload_dir/$changes_f" )) { + local($/); undef $/; + my $changetext = <F>; + close( F ); + push( @to_remove, $self->get_files_from_changes( $changetext ) ); + } else { + foreach (split( "\n", $self->get('Mail Body Text'))) { + if (/^(\[-+~\.\w]+\.(u?deb))$/) { + my $f = $1; + push( @to_remove, $f ) if !grep { $_ eq $f } @to_remove; + } + } + } + ($upload_f = $changes_f) =~ s/\.changes$/\.upload/; + push( @to_remove, $changes_f, $upload_f ); + ($pkgv = $changes_f) =~ s/_(\S+)\.changes//; + $self->log("$pkgv has been installed; removing from upload dir:\n", + "@to_remove\n"); + + my @dists; + if (open( F, "<$upload_dir/$changes_f" )) { + my $changes_text; + { local($/); undef $/; $changes_text = <F>; } + close( F ); + @dists = $self->get_dists_from_changes( $changes_text ); + } else { + $self->log("Cannot get dists from $upload_dir/$changes_f: $! (assuming unstable)\n"); + @dists = ( "unstable" ); + } + +FILE: foreach (@to_remove) { + if (/\.deb$/) { + # first listed wins + foreach my $dist (@dists) { + if ( -d $self->get_conf('HOME') . "/build/chroot-$dist" && + -w $self->get_conf('HOME') . "/build/chroot-$dist/var/cache/apt/archives/") { + # TODO: send all of to_remove to perl-apt if it's available, setting a try_mv list + # that only has build-depends in it. + # if that's too much cpu, have buildd use perl-apt if avail to export the + # build-depends list, which could then be read in at this point + if (system (qw(mv --), "$upload_dir/$_", + $self->get_conf('HOME') . + "/build/chroot-$dist/var/cache/apt/archives/")) { + $self->log("Cannot move $upload_dir/$_ to cache dir\n"); + } else { + next FILE; + } + } + } + } + unlink "$upload_dir/$_" + or $self->log("Can't remove $upload_dir/$_: $!\n"); +} + } + elsif ($subject =~ /^(\S+\.changes) is NEW$/) { + # "is new" mail from dinstall + my $changes_f = $1; + my $pkgv; + ($pkgv = $changes_f) =~ s/_(\S+)\.changes//; + $self->log("$pkgv must be manually dinstall-ed -- delayed\n"); + } + elsif ($subject =~ /^new version of (\S+) \(dist=(\S+)\)$/) { + # notice from wanna-build + my ($pkg, $dist_name) = ($1, $2); + my $dist_config = $self->get_dist_config_by_name($dist_name); + goto forward if $self->get('Mail Body Text') !~ /^in version (\S+)\.$/m; + my $pkgv = $pkg."_".$1; + $self->get('Mail Body Text') =~ /new source version (\S+)\./m; + my $newv = $1; + $self->log("Build of $pkgv ($dist_name) obsolete -- new version $newv\n"); + $self->register_outdated( $dist_name, $pkgv, $pkg."_".$newv ); + + my @ds; + if (!(@ds = $self->check_building_any_dist( $pkgv ))) { + if (!$self->remove_from_REDO( $pkgv )) { + $self->append_to_SKIP( $pkgv ); + } + $self->purge_pkg( $pkgv, $dist_config ); + } + else { + $self->log("Not deleting, still building for @ds\n"); + } + } + elsif ($self->get('Mail Body Text') =~ /^blacklist (\S+)\n$/) { + my $pattern = "\Q$1\E"; + if (open( F, ">>mail-blacklist" )) { + print F "$pattern\n"; + close( F ); + $self->log("Added $pattern to blacklist.\n"); + } + else { + $self->log("Can't open mail-blacklist for appending: $!\n"); + } + } + else { + goto forward_mail; + } + + + if ($self->get('Mail Error')) { + $self->log("Error: ", + $self->get('Mail Short Error') || $self->get('Mail Error')); + $self->reply("Your mail could not be processed:\n" . + $self->get('Mail Error')); + } + return; + +forward_mail: + my $header = $self->get('Mail Header'); + $self->log("Mail from $header->{'from'}\nSubject: $subject\n"); + if ($self->is_blacklisted( $self->get('Mail Header')->{'from'} )) { + $self->log("Address is blacklisted, deleting mail.\n"); + } + else { + $self->log("Not for me, forwarding to admin\n"); + ll_send_mail( $self->get_conf('ADMIN_MAIL'), + "To: $header->{'to'}\n". + ($header->{'cc'} ? "Cc: $header->{'cc'}\n" : ""). + "From: $header->{'from'}\n". + "Subject: $header->{'subject'}\n". + "Date: $header->{'date'}\n". + "Message-Id: $header->{'message-id'}\n". + ($header->{'reply-to'} ? "Reply-To: $header->{'reply-to'}\n" : ""). + ($header->{'in-reply-to'} ? "In-Reply-To: $header->{'in-reply-to'}\n" : ""). + ($header->{'references'} ? "References: $header->{'references'}\n" : ""). + ($header->{'content-type'} ? "Content-Type: $header->{'content-type'}\n": ""). + "Resent-From: $Buildd::gecos <$Buildd::username\@$Buildd::hostname>\n". + "Resent-To: " . $self->get_conf('ADMIN_MAIL') . "\n\n". + $self->get('Mail Body Text') ); + } +} + + +sub prepare_for_upload ($$) { + my $self = shift; + my $pkg = shift; + my $changes = shift; + + $changes =~ s/\n+$/\n/; + + my( @files, @md5, @missing, @md5fail, $i ); + + my @to_dists = $self->get_dists_from_changes( $changes ); + if (!@to_dists) { # probably not a valid changes + + $self->set('Mail Short Error', + $self->get('Mail Error')); + $self->set('Mail Error', + $self->get('Mail Error') . + "Couldn't find a valid Distribution: line.\n"); + return 0; + } + + my $changes_filename_arch = $self->get_conf('ARCH'); + #Try to extract the arch from the actual changes file (see #566398) + if ($changes =~ /^Architecture:\s*(.+)/m) { + my @arches = grep { $_ ne "all" } split /\s+/, $1; + if (@arches > 1) { + $changes_filename_arch = "multi"; + } else { + $changes_filename_arch = $arches[0]; + } + } + + $changes =~ /^Files:\s*\n((^[ ]+.*\n)*)/m; + foreach (split( "\n", $1 )) { + push( @md5, (split( /\s+/, $_ ))[1] ); + push( @files, (split( /\s+/, $_ ))[5] ); + } + if (!@files) { # probably not a valid changes + $self->set('Mail Short Error', + $self->get('Mail Error')); + $self->set('Mail Error', + $self->get('Mail Error') . + "No files listed in changes.\n"); + return 0; + } + my @wrong_dists = (); + foreach my $d (@to_dists) { + push( @wrong_dists, $d ) + if !$self->check_state( + $pkg, + $self->get_dist_config_by_name($d), + qw(Building Built Install-Wait Reupload-Wait Build-Attempted)); + } + if (@wrong_dists) { + $self->set('Mail Short Error', + $self->get('Mail Error')); + $self->set('Mail Error', + $self->get('Mail Error') . + "Package $pkg has target distributions @wrong_dists\n". + "for which it isn't registered as Building.\n". + "Please fix this by either modifying the Distribution: ". + "header or\n". + "taking the package in those distributions, too.\n"); + return 0; + } + + for( $i = 0; $i < @files; ++$i ) { + if (! -f $self->get_conf('HOME') . "/build/$files[$i]") { + push( @missing, $files[$i] ) ; + } + else { + my $home = $self->get_conf('HOME'); + chomp( my $sum = `md5sum $home/build/$files[$i]` ); + push( @md5fail, $files[$i] ) if (split(/\s+/,$sum))[0] ne $md5[$i]; + } + } + if (@missing) { + $self->set('Mail Short Error', + $self->get('Mail Short Error') . + "Missing files for move: @missing\n"); + $self->set('Mail Error', + $self->get('Mail Error') . + "While trying to move the built package $pkg to upload,\n". + "the following files mentioned in the .changes were not found:\n". + "@missing\n"); + return 0; + } + if (@md5fail) { + $self->set('Mail Short Error', + $self->get('Mail Short Error') . + "md5 failure during move: @md5fail\n"); + $self->set('Mail Error', + $self->get('Mail Error') . + "While trying to move the built package $pkg to upload,\n". + "the following files had bad md5 checksums:\n". + "@md5fail\n"); + return 0; + } + + my @upload_dirs = $self->get_upload_queue_dirs ( $changes ); + + my $pkg_noep = $pkg; + $pkg_noep =~ s/_\d*:/_/; + my $changes_name = $pkg_noep . "_" . $changes_filename_arch . ".changes"; + + for my $upload_dir (@upload_dirs) { + if (! -d $upload_dir &&!mkdir( $upload_dir, 0750 )) { + $self->set('Mail Error', + $self->get('Mail Error') . + "Cannot create directory $upload_dir"); + $self->log("Cannot create dir $upload_dir\n"); + return 0; + } + } + + my $errs = 0; + for my $upload_dir (@upload_dirs) { + lock_file( $upload_dir ); + foreach (@files) { + if (system('cp', '--', $self->get_conf('HOME')."/build/$_", "$upload_dir/$_")) { + $self->log("Cannot copy $_ to $upload_dir/\n"); + ++$errs; + } + } + + open( F, ">$upload_dir/$changes_name" ); + print F $changes; + close( F ); + unlock_file( $upload_dir ); + $self->log("Moved $pkg to ", basename($upload_dir), "\n"); + } + + foreach (@files) { + if (!unlink($self->get_conf('HOME') . "/build/$_")) { + $self->log("Cannot remove build/$_\n"); + ++$errs; + } + } + + if ($errs) { + $self->set('Mail Error', + $self->get('Mail Error') . + "Could not move all files to upload dir."); + return 0; + } + + unlink( $self->get_conf('HOME') . "/build/$changes_name" ) + or $self->log("Cannot remove " . $self->get_conf('HOME') . "/$changes_name: $!\n"); +} + +sub redo_new_version ($$$) { + my $self = shift; + my $dist_config = shift; + my $oldv = shift; + my $newv = shift; + + my $err = 0; + + my $db = $self->get_db_handle($dist_config); + my $pipe = $db->pipe_query('-v', $newv); + if ($pipe) { + while(<$pipe>) { + next if /^wanna-build Revision/ || + /^\S+: Warning: Older version / || + /^\S+: ok$/; + $self->set('Mail Error', + $self->get('Mail Error') . $_); + $err = 1; + } + close($pipe); + } else { + $self->log("Can't spawn wanna-build: $!\n"); + $self->set('Mail Error', + $self->get('Mail Error') . + "Can't spawn wanna-build: $!\n"); + return; + } + if ($err) { + $self->log("Can't take newer version $newv due to wanna-build errors\n"); + return; + } + $self->log("Going to build $newv instead of $oldv\n"); + + $self->append_to_REDO( $newv, $dist_config ); +} + +sub purge_pkg ($$) { + my $self = shift; + my $pkg = shift; + my $dist_config = shift; + my $dist_name = $dist_config->get('DIST_NAME'); + + my $dir; + local( *F ); + + $self->remove_from_REDO( $pkg ); + + # remove .changes and .deb in build dir (if existing) + my $pkg_noep = $pkg; + $pkg_noep =~ s/_\d*:/_/; + my $changes = "${pkg_noep}_" . $self->get_conf('ARCH') . ".changes"; + if (-f "build/$changes" && open( F, "<build/$changes" )) { + local($/); undef $/; + my $changetext = <F>; + close( F ); + my @files = $self->get_files_from_changes( $changetext ); + push( @files, $changes ); + $self->log("Purging files: $changes\n"); + unlink( map { "build/$_" } @files ); + } + + # schedule dir for purging + ($dir = $pkg_noep) =~ s/-[^-]*$//; # remove Debian revision + $dir =~ s/_/-/; # change _ to - + if (-d "build/chroot-$dist_name/build/$Buildd::username/$dir") { + $dir = "build/chroot-$dist_name/build/$Buildd::username/$dir"; + } + else { + $dir = "build/$dir"; + } + return if ! -d $dir; + + lock_file( "build/PURGE" ); + if (open( F, ">>build/PURGE" )) { + print F "$dir\n"; + close( F ); + $self->log("Scheduled $dir for purging\n"); + } + else { + $self->set('Mail Error', + $self->get('Mail Error') . + "Can't open build/PURGE: $!\n"); + $self->log("Can't open build/PURGE: $!\n"); + } + unlock_file( "build/PURGE" ); +} + +sub remove_from_upload ($) { + my $self = shift; + my $pkg = shift; + my $dist_config = shift; + + my($changes_f, $upload_f, $changes_text, @to_remove); + local( *F ); + + $self->log("Remove $pkg from upload dir\n"); + my $pkg_noep = $pkg; + $pkg_noep =~ s/_\d*:/_/; + $changes_f = "${pkg_noep}_" . $self->get_conf('ARCH') . ".changes"; + + my $upload_dir = $self->get_conf('HOME') . '/' . $dist_config->get('DUPLOAD_LOCAL_QUEUE_DIR'); + + if (!-f "$upload_dir/$changes_f") { + $self->log("$changes_f does not exist\n"); + return; + } + if (!open( F, "<$upload_dir/$changes_f" )) { + $self->log("Cannot open $upload_dir/$changes_f: $!\n"); + return; + } + { local($/); undef $/; $changes_text = <F>; } + close( F ); + @to_remove = $self->get_files_from_changes( $changes_text ); + + ($upload_f = $changes_f) =~ s/\.changes$/\.upload/; + push( @to_remove, $changes_f, $upload_f ); + + $self->log("Removing files:\n", "@to_remove\n"); + foreach (@to_remove) { + unlink "$upload_dir/$_" + or $self->log("Can't remove $upload_dir/$_: $!\n"); + } +} + +sub append_to_REDO ($$) { + my $self = shift; + my $pkg = shift; + my $dist_config = shift; + my $dist_name = $dist_config->get('DIST_NAME'); + + local( *F ); + + lock_file( "build/REDO" ); + + if (open( F, "build/REDO" )) { + my @pkgs = <F>; + close( F ); + if (grep( /^\Q$pkg\E\s/, @pkgs )) { + $self->log("$pkg is already in REDO -- not rescheduled\n"); + goto unlock; + } + } + + if (open( F, ">>build/REDO" )) { + print F "$pkg $dist_name\n"; + close( F ); + $self->log("Scheduled $pkg for rebuild\n"); + } + else { + $self->set('Mail Error', + $self->get('Mail Error') . + "Can't open build/REDO: $!\n"); + $self->log("Can't open build/REDO: $!\n"); + } + + unlock: + unlock_file( "build/REDO" ); +} + +sub remove_from_REDO ($) { + my $self = shift; + my $pkg = shift; + + local( *F ); + + lock_file( "build/REDO" ); + goto unlock if !open( F, "<build/REDO" ); + my @pkgs = <F>; + close( F ); + if (!open( F, ">build/REDO" )) { + $self->log("Can't open REDO for writing: $!\n", + "Would write: @pkgs\nminus $pkg\n"); + goto unlock; + } + my $done = 0; + foreach (@pkgs) { + if (/^\Q$pkg\E\s/) { + ++$done; + } + else { + print F $_; + } + } + close( F ); + $self->log("Deleted $pkg from REDO list.\n") if $done; + unlock: + unlock_file( "build/REDO" ); + return $done; +} + +sub append_to_SKIP ($) { + my $self = shift; + my $pkg = shift; + + local( *F ); + + return if !open( F, "<build/build-progress" ); + my @lines = <F>; + close( F ); + + if (grep( /^\s*\Q$pkg\E$/, @lines )) { + # pkg is in build-progress, but without a suffix (failed, + # successful, currently building), so it can be skipped + lock_file( "build/SKIP" ); + if (open( F, ">>build/SKIP" )) { + print F "$pkg\n"; + close( F ); + $self->log("Told sbuild to skip $pkg\n"); + } + unlock_file( "build/SKIP" ); + } +} + +sub check_is_outdated ($$) { + my $self = shift; + my $dist_config = shift; + my $package = shift; + my $dist_name = $dist_config->get('DIST_NAME'); + + my %newv; + return 0 if !(%newv = $self->is_outdated( $dist_name, $package )); + + my $have_changes = 1 if $self->get('Mail Body Text') =~ /^---+\s*BEGIN PGP SIGNED MESSAGE/; + + # If we have a changes file, we can see which distributions that + # package is aimed to. Otherwise, we're out of luck because we can't see + # reliably anymore for which distribs the package was for. Let the user + # find out this... + # + # If the package is outdated in all dists we have to consider, + # send a plain error message. If only outdated in some of them, send a + # modified error that tells to send a restricted changes (with + # Distribution: only for those dists where it isn't outdated), or to do + # the action manually, because it would be (wrongly) propagated. + goto all_outdated if !$have_changes; + + my @check_dists = (); + @check_dists = $self->get_dists_from_changes($self->get('Mail Body Text')); + + my @not_outdated = (); + my @outdated = (); + foreach (@check_dists) { + if (!exists $newv{$_}) { + push( @not_outdated, $_ ); + } + else { + push( @outdated, $_ ); + } + } + return 0 if !@outdated; + if (@not_outdated) { + $self->set('Mail Short Error', + $self->get('Mail Short Error') . + "$package ($dist_name) partially outdated ". + "(ok for @not_outdated)\n"); + $self->set('Mail Error', + $self->get('Mail Error') . + "Package $package ($dist_name) is partially outdated.\n". + "The following new versions have appeared in the meantime:\n ". + join( "\n ", map { "$_: $newv{$_}" } keys %newv )."\n\n". + "Please send a .changes for the following distributions only:\n". + " Distribution: ".join( " ", @not_outdated )."\n"); + } + else { + all_outdated: + $self->set('Mail Short Error', + $self->get('Mail Short Error') . + "$package ($dist_name) outdated; new versions ". + join( ", ", map { "$_:$newv{$_}" } keys %newv )."\n"); + $self->set('Mail Error', + $self->get('Mail Error') . + "Package $package ($dist_name) is outdated.\n". + "The following new versions have appeared in the meantime:\n ". + join( "\n ", map { "$_: $newv{$_}" } keys %newv )."\n"); + } + return 1; +} + +sub is_outdated ($$) { + my $self = shift; + my $dist_name = shift; + my $pkg = shift; + + my %result = (); + local( *F ); + + lock_file( "outdated-packages" ); + goto unlock if !open( F, "<outdated-packages" ); + while( <F> ) { + my($oldpkg, $newpkg, $t, $d) = split( /\s+/, $_ ); + $d ||= "unstable"; + if ($oldpkg eq $pkg && $d eq $dist_name) { + $result{$d} = $newpkg; + } + } + close( F ); + unlock: + unlock_file( "outdated-packages" ); + return %result; +} + +sub register_outdated ($$$) { + my $self = shift; + my $dist = shift; + my $oldv = shift; + my $newv = shift; + + my(@pkgs); + local( *F ); + + lock_file( "outdated-packages" ); + + if (open( F, "<outdated-packages" )) { + @pkgs = <F>; + close( F ); + } + + if (!open( F, ">outdated-packages" )) { + $self->log("Cannot open outdated-packages for writing: $!\n"); + goto unlock; + } + my $now = time; + my @d = (); + foreach (@pkgs) { + my($oldpkg, $newpkg, $t, $d) = split( /\s+/, $_ ); + $d ||= "unstable"; + next if ($oldpkg eq $oldv && $d eq $dist) || ($now - $t) > 21*24*60*60; + print F $_; + } + print F "$oldv $newv $now $dist\n"; + close( F ); + unlock: + unlock_file( "outdated-packages" ); +} + +sub set_to_failed ($$$) { + my $self = shift; + my $pkg = shift; + my $dist_config = shift; + my $text = shift; + my $dist_name = $dist_config->get('DIST_NAME'); + + my $is_bugno = 0; + + $text =~ s/^\.$/../mg; + $is_bugno = 1 if $text =~ /^\(see #\d+\)$/; + return if !$self->check_state( $pkg, $dist_config, $is_bugno ? "Failed" : qw(Built Building Build-Attempted BD-Uninstallable) ); + + my $db = $self->get_db_handle($dist_config); + my $pipe = $db->pipe_query_out('--failed', $pkg); + if ($pipe) { + print $pipe "${text}.\n"; + close($pipe); + } + if ($?) { + my $t = "wanna-build --failed failed with status ".exitstatus($?)."\n"; + $self->log($t); + $self->set('Mail Error', + $self->get('Mail Error') . $t); + } elsif ($is_bugno) { + $self->log("Bug# appended to fail message of $pkg ($dist_name)\n"); + } + else { + $self->log("Set package $pkg ($dist_name) to Failed\n"); + $self->write_stats("failed", 1); + } +} + +sub set_to_depwait ($$$) { + my $self = shift; + my $pkg = shift; + my $dist_config = shift; + my $deps = shift; + my $dist_name = $dist_config->get('DIST_NAME'); + + my $db = $self->get_db_handle($dist_config); + my $pipe = $db->pipe_query_out('--dep-wait', $pkg); + if ($pipe) { + print $pipe "$deps\n"; + close($pipe); + } + if ($?) { + my $t = "wanna-build --dep-wait failed with status ".exitstatus($?)."\n"; + $self->log($t); + $self->set('Mail Error', + $self->get('Mail Error') . $t); + } + else { + $self->log("Set package $pkg ($dist_name) to Dep-Wait\nDependencies: $deps\n"); + } + $self->write_stats("dep-wait", 1); +} + +sub give_back ($$) { + my $self = shift; + my $pkg = shift; + my $dist_config = shift; + my $dist_name = $dist_config->get('DIST_NAME'); + + my $answer; + + my $db = $self->get_db_handle($dist_config); + my $pipe = $db->pipe_query('--give-back', $pkg); + if ($pipe) { + $answer = <$pipe>; + close($pipe); + } + if ($?) { + $self->set('Mail Error', + $self->get('Mail Error') . + "wanna-build --give-back failed:\n$answer"); + } + else { + $self->log("Given back package $pkg ($dist_name)\n"); + } +} + +sub no_build ($$) { + my $self = shift; + my $pkg = shift; + my $dist_config = shift; + my $dist_name = $dist_config->get('DIST_NAME'); + my $answer_cmd; + + my $answer; + + my $db = $self->get_db_handle($dist_config); + my $pipe = $db->pipe_query('--no-build', $pkg); + if ($pipe) { + $answer = <$pipe>; + close($pipe); + } + if ($?) { + $self->set('Mail Error', + $self->get('Mail Error') . + "no-build failed:\n$answer"); + } + else { + $self->log("Package $pkg ($dist_name) to set Not-For-Us\n"); + } + $self->write_stats("no-build", 1); +} + +sub get_fail_msg ($$) { + my $self = shift; + my $pkg = shift; + my $dist_config = shift; + my $dist_name = $dist_config->get('DIST_NAME'); + + $pkg =~ s/_.*//; + + my $db = $self->get_db_handle($dist_config); + my $pipe = $db->pipe_query('--info', $pkg); + if ($pipe) { + my $msg = ""; + while(<$pipe>) { + if (/^\s*Old-Failed\s*:/) { + while(<$pipe>) { + last if /^ \S+\s*/; + $_ =~ s/^\s+//; + if (/^----+\s+\S+\s+----+$/) { + last if $msg; + } + else { + $msg .= $_; + } + } + last; + } + } + close($pipe); + return $msg if $msg; + $self->set('Mail Error', + $self->get('Mail Error') . + "Couldn't find Old-Failed in info for $pkg\n"); + return "Same as previous version (couldn't extract the text)\n"; + } else { + $self->set('Mail Error', + $self->get('Mail Error') . + "Couldn't start wanna-build --info: $!\n"); + return "Same as previous version (couldn't extract the text)\n"; + } +} + +sub check_state ($@) { + my $self = shift; + my $mail_error = $self->get('Mail Error'); + my $retval = $self->check_state_internal(@_); + # check if we should retry the call + if ($retval == -1) { + my $interval = int(rand(120)); + $self->log("Retrying --info in $interval seconds...\n"); + # reset error to old value + $self->set('Mail Error', $mail_error); + # 0..120s of sleep ought to be enough for retrying; + # for mail bursts, this should get us out of the + # crticial mass + sleep $interval; + $retval = $self->check_state_internal(@_); + # remap the -1 retry code to failure + if ($retval == -1) { + return 0; + } else { + return $retval; + } + } + return $retval; +} + +sub check_state_internal ($$@) { + my $self = shift; + my $pkgv = shift; + my $dist_config = shift; + my @wanted_states = @_; + my $dist_name = $dist_config->get('DIST_NAME'); + + $pkgv =~ /^([^_]+)_(.+)/; + my ($pkg, $vers) = ($1, $2); + + my $db = $self->get_db_handle($dist_config); + my $pipe = $db->pipe_query('--info', $pkg); + if (!$pipe) { + $self->set('Mail Error', + $self->get('Mail Error') . + "Couldn't start wanna-build --info: $!\n"); + $self->log("Couldn't start wanna-build --info: $!\n"); + # let check_state() retry if needed + return -1; + } + + my ($av, $as, $ab, $an); + while(<$pipe>) { + $av = $1 if /^\s*Version\s*:\s*(\S+)/; + $as = $1 if /^\s*State\s*:\s*(\S+)/; + $ab = $1 if /^\s*Builder\s*:\s*(\S+)/; + $an = $1 if /^\s*Binary-NMU-Version\s*:\s*(\d+)/; + } + close($pipe); + + if ($?) { + my $t = "wanna-build --info failed with status ".exitstatus($?)."\n"; + $self->log($t); + $self->set('Mail Error', + $self->get('Mail Error') . $t); + return 0; + } + + my $msg = "$pkgv($dist_name) check_state(@wanted_states): "; + $av = binNMU_version($av,$an,undef) if (defined $an); + if ($av ne $vers) { + $self->set('Mail Error', + $self->get('Mail Error') . + $msg."version $av registered as $as\n"); + return 0; + } + if (!Buildd::isin( $as, @wanted_states)) { + $self->set('Mail Error', + $self->get('Mail Error') . + $msg."state is $as\n"); + return 0; + } + if ($as eq "Building" && $ab ne $dist_config->get('WANNA_BUILD_DB_USER')) { + $self->set('Mail Error', + $self->get('Mail Error') . + $msg."is building by $ab\n"); + return 0; + } + return 1; +} + +sub check_building_any_dist ($) { + my $self = shift; + my $pkgv = shift; + + my @dists; + + $pkgv =~ /^([^_]+)_(.+)/; + my ($pkg, $vers) = ($1, $2); + + for my $dist_config (@{$self->get_conf('DISTRIBUTIONS')}) { + my $dist_name = $dist_config->get('DIST_NAME'); + + my $db = $self->get_db_handle($dist_config); + my $pipe = $db->pipe_query('--info', $pkg); + if (!$pipe) { + $self->set('Mail Error', + $self->get('Mail Error') . + "Couldn't start wanna-build --info: $!\n"); + return 0; + } + + my $text; + { local ($/); $text = <$pipe>; } + close($pipe); + + while( $text =~ /^\Q$pkg\E\((\w+)\):(.*)\n((\s.*\n)*)/mg ) { + my ($dist, $rest, $info) = ($1, $2, $3); + next if $rest =~ /not registered/; + my ($av, $as, $ab); + $av = $1 if $info =~ /^\s*Version\s*:\s*(\S+)/mi; + $as = $1 if $info =~ /^\s*State\s*:\s*(\S+)/mi; + $ab = $1 if $info =~ /^\s*Builder\s*:\s*(\S+)/mi; + push( @dists, $dist ) + if $av eq $vers && $as eq "Building" && + $ab eq $self->get_conf('WANNA_BUILD_DB_USER'); + } + } + return @dists; +} + +sub get_files_from_changes ($) { + my $self = shift; + my $changes_text = shift; + + my(@filelines, @files); + + $changes_text =~ /^Files:\s*\n((^[ ]+.*\n)*)/m; + @filelines = split( "\n", $1 ); + foreach (@filelines) { + push( @files, (split( /\s+/, $_ ))[5] ); + } + return @files; +} + +sub get_dists_from_changes ($) { + my $self = shift; + my $changes_text = shift; + + $changes_text =~ /^Distribution:\s*(.*)\s*$/mi; + return split( /\s+/, $1 ); +} + +sub get_upload_queue_dirs ($) { + my $self = shift; + my $changes_text = shift; + + my %upload_dirs; + my @dists = $self->get_dists_from_changes( $changes_text ); + for my $dist_config (@{$self->get_conf('DISTRIBUTIONS')}) { + my $upload_dir = $self->get_conf('HOME') . '/' . $dist_config->get('DUPLOAD_LOCAL_QUEUE_DIR'); + + if (grep { $dist_config->get('DIST_NAME') eq $_ } @dists) { + $upload_dirs{$upload_dir} = 1; + } + } + return keys %upload_dirs; +} + +sub find_upload_dirs_for_changes_file ($) { + my $self = shift; + my $changes_file_name = shift; + + my %dirs; + + for my $dist_config (@{$self->get_conf('DISTRIBUTIONS')}) { + my $upload_dir = $self->get_conf('HOME') . '/' . $dist_config->get('DUPLOAD_LOCAL_QUEUE_DIR'); + if (-f "$upload_dir/$changes_file_name") { + $dirs{$upload_dir} = 1; + } + } + + return keys %dirs; +} + +sub reply ($) { + my $self = shift; + my $text = shift; + + my( $to, $subj, $quoting ); + + $to = $self->get('Mail Header')->{'reply-to'} || + $self->get('Mail Header')->{'from'}; + $subj = $self->get('Mail Header')->{'subject'}; + $subj = "Re: $subj" if $subj !~ /^Re\S{0,2}:/; + ($quoting = $self->get('Mail Body Text')) =~ s/\n+$/\n/; + $quoting =~ s/^/> /mg; + + send_mail( $to, $subj, "$text\n$quoting", + "In-Reply-To: ". $self->get('Mail Header')->{'message-id'}. "\n" ); +} + +sub is_blacklisted ($) { + my $self = shift; + my $addr = shift; + + local( *BL ); + + $addr = $1 if $addr =~ /<(.*)>/; + return 0 if !open( BL, "<mail-blacklist" ); + while( <BL> ) { + chomp; + if ($addr =~ /$_$/) { + close( BL ); + return 1; + } + } + close( BL ); + return 0; +} + +sub add_error_mail () { + my $self = shift; + + local( *F ); + my $now = time; + my @em = (); + + if (open( F, "<mail-errormails" )) { + chomp( @em = <F> ); + close( F ); + } + push( @em, $now ); + shift @em while @em && ($now - $em[0]) > $self->get_conf('ERROR_MAIL_WINDOW'); + + if (@em) { + open( F, ">mail-errormails" ); + print F join( "\n", @em ), "\n"; + close( F ); + } + else { + unlink( "mail-errormails" ); + } + + return scalar(@em); +} + +1; |