#!/usr/bin/perl -w
#
# Copyright (c) 2009 Michael Schroeder, Novell Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 as
# published by the Free Software Foundation.
#
# 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 (see the file COPYING); if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
#
################################################################
#
# Sign the built packages
#

BEGIN {
  my ($wd) = $0 =~ m-(.*)/- ;
  $wd ||= '.';
  unshift @INC,  "$wd/build";
  unshift @INC,  "$wd";
}

use POSIX;
use Data::Dumper;
use Digest::MD5 ();
use Fcntl qw(:DEFAULT :flock);
use XML::Structured ':bytes';
use Build;
use Storable;

use BSConfig;
use BSRPC;
use BSUtil;
use BSXML;
use BSVerify;

use strict;

my $user = $BSConfig::bsuser;
my $group = $BSConfig::bsgroup;

!defined($user) || defined($user = (getpwnam($user))[2]) || die("unknown user\n");
!defined($group) || defined($group = (getgrnam($group))[2]) || die("unknown group\n");
if (defined $group) {
  ($), $() = ($group, $group);
  die "setgid: $!\n" if ($) != $group);
}
if (defined $user) {
  ($>, $<) = ($user, $user);
  die "setuid: $!\n" if ($> != $user);
}

my $rundir = $BSConfig::rundir || "$BSConfig::bsdir/run";
my $jobsdir = "$BSConfig::bsdir/jobs";
my $eventdir = "$BSConfig::bsdir/events";
my $myeventdir = "$eventdir/signer";
my $uploaddir = "$BSConfig::bsdir/upload";

my $maxchild = 4;

sub signjob {
  my ($job, $arch) = @_;

  print "signing $arch/$job\n";
  local *F;
  if (! -e "$jobsdir/$arch/$job") {
    print "no such job\n";
    return undef;
  }
  if (! -e "$jobsdir/$arch/$job:status") {
    print "job is not done\n";
    return undef;
  }
  my $jobstatus = BSUtil::lockopenxml(\*F, '<', "$jobsdir/$arch/$job:status", $BSXML::jobstatus);
  # finished can be removed here later, but running jobs shall not be lost on code update.
  if ($jobstatus->{'code'} ne 'finished' && $jobstatus->{'code'} ne 'signing') {
    print "job is not assigned for signing\n";
    close F;
    return undef;
  }
  my $jobdir = "$jobsdir/$arch/$job:dir";
  die("jobdir does not exist\n") unless -d $jobdir;
  my $info = readxml("$jobsdir/$arch/$job", $BSXML::buildinfo);
  my $projid = $info->{'project'};
  my @files = sort(ls($jobdir));
  my @signfiles = grep {/\.(?:rpm|sha256)$/} @files;
  if (@signfiles) {
    my @signargs;
    push @signargs, '--project', $projid if $BSConfig::sign_project;
    my $signkey = BSRPC::rpc("$BSConfig::srcserver/getsignkey", undef, "project=$projid");
    if ($signkey) {
      mkdir_p($uploaddir);
      writestr("$uploaddir/signer.$$", undef, $signkey);
      push @signargs, '-P', "$uploaddir/signer.$$";
    }
    for my $signfile (@signfiles) {
      if ($info->{'file'} eq '_aggregate') {
	# special aggregate handling: remove old sigs
        system('rpm', '--delsign', "$jobdir/$signfile") && warn("delsign $jobdir/$signfile failed: $?\n");
      }
      if (system($BSConfig::sign, @signargs, "$jobdir/$signfile")) {
        unlink("$uploaddir/signer.$$") if $signkey;
	close F;
	die("sign $jobdir/$signfile failed: $?\n");
      }
    }
    unlink("$uploaddir/signer.$$") if $signkey;

    # we have changed the file ids, thus we need to re-create
    # the .bininfo file
    my $bininfo = {};
    for my $file (@files) {
      next unless $file =~ /\.(?:rpm|deb)$/;
      my @s = stat("$jobdir/$file");
      next unless @s;
      my $id = "$s[9]/$s[7]/$s[1]";
      my $data = Build::query("$jobdir/$file", 'evra' => 1);
      eval {
        BSVerify::verify_nevraquery($data);
      };
      die("$jobdir/$file: $@") if $@;
      $bininfo->{$id} = $data;
    }
    Storable::nstore($bininfo, "$jobdir/.bininfo") if %$bininfo;
  }
  
  # write finished job status and release lock
  $jobstatus->{'code'} = 'finished';
  writexml("$jobsdir/$arch/.$job:status", "$jobsdir/$arch/$job:status", $jobstatus, $BSXML::jobstatus);
  close F;

  return 1;
}

sub ping {
  my ($arch) = @_;
  local *F;
  if (sysopen(F, "$eventdir/$arch/.ping", POSIX::O_WRONLY|POSIX::O_NONBLOCK)) {
    syswrite(F, 'x');
    close(F);
  }
}

sub signevent {
  my ($event, $ev) = @_;

  rename("$myeventdir/$event", "$myeventdir/${event}::inprogress");
  my $job = $ev->{'job'};
  my $arch = $ev->{'arch'};
  my $res;
  eval {
    $res = signjob($job, $arch);
  };
  if ($@) {
    warn("sign failed: $@");
    rename("$myeventdir/${event}::inprogress", "$myeventdir/$event");
    return;
  } elsif ($res) {
    my $ev = {'type' => 'built', 'arch' => $arch, 'job' => $job};
    writexml("$eventdir/$arch/.finished:$job$$", "$eventdir/$arch/finished:$job", $ev, $BSXML::event);
    ping($arch);
  }
  unlink("$myeventdir/${event}::inprogress");
}

$| = 1;
$SIG{'PIPE'} = 'IGNORE';
print "starting build service signer\n";

# get lock
mkdir_p($rundir);
open(RUNLOCK, '>>', "$rundir/bs_signer.lock") || die("$rundir/bs_signer.lock: $!\n");
flock(RUNLOCK, LOCK_EX | LOCK_NB) || die("signer is already running!\n");
utime undef, undef, "$rundir/bs_signer.lock";

die("sign program is not configured!\n") unless $BSConfig::sign;

mkdir_p($myeventdir);
if (!-p "$myeventdir/.ping") {
  POSIX::mkfifo("$myeventdir/.ping", 0666) || die("$myeventdir/.ping: $!");
  chmod(0666, "$myeventdir/.ping");
}
sysopen(PING, "$myeventdir/.ping", POSIX::O_RDWR) || die("$myeventdir/.ping: $!");

for my $event (grep {s/::inprogress$//s} ls($myeventdir)) {
  rename("$myeventdir/${event}::inprogress", "$myeventdir/$event");
}

my %chld;
my $pid;

while (1) {
  # drain ping pipe
  my $dummy;
  fcntl(PING,F_SETFL,POSIX::O_NONBLOCK);
  1 while (sysread(PING, $dummy, 1024, 0) || 0) > 0; 
  fcntl(PING,F_SETFL,0);

  my @events = ls($myeventdir);
  @events = grep {!/^\./} @events;

  for my $event (@events) {
    last if -e "$rundir/bs_signer.exit";
    last if -e "$rundir/bs_signer.restart";

    my $ev = readxml("$myeventdir/$event", $BSXML::event, 1);
    if (!$ev) {
      unlink("$myeventdir/$event");
      next;
    }
    if ($ev->{'type'} ne 'built') {
      print "unknown event type: $ev->{'type'}\n";
      unlink("$myeventdir/$event");
      next;
    }
    if (!$maxchild || $maxchild == 1) {
      signevent($event, $ev);
      next;
    }
    if (!($pid = xfork())) {
      signevent($event, $ev);
      exit(0);
    }
    $chld{$pid} = 1;
    while (($pid = waitpid(-1, defined($maxchild) && keys(%chld) > $maxchild ? 0 : POSIX::WNOHANG)) > 0) {
      delete $chld{$pid};
    }
  }

  if (%chld) {
    while (($pid = waitpid(-1, 0)) > 0) {
      delete $chld{$pid};
    }   
  }

  if (-e "$rundir/bs_signer.exit") {
    unlink("$rundir/bs_signer.exit");
    print "exiting...\n";
    exit(0);
  }
  if (-e "$rundir/bs_signer.restart") {
    unlink("$rundir/bs_signer.restart");
    print "restarting...\n";
    exec($0);
    die("$0: $!\n");
  }
  print "waiting for an event...\n";
  sysread(PING, $dummy, 1, 0);
}
