#!/usr/bin/perl -w
#
# Copyright (c) 2006, 2007 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
#
################################################################
#
# Worker build process. Builds jobs received from a Repository Server,
# sends build binary packages back.
#

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

use BSRPC;
use BSServer;
use BSConfig;
use BSUtil;
use BSXML;
use BSHTTP;
use BSBuild;

use strict;

my $buildroot;
my $port;
my $statedir;
my $hostarch;
my $xen = '';
my $xen_root = '';
my $xen_swap = '';
my $workerid;
my $srcserver;
my @reposervers;
my $testmode;
my $noworkercheck;
my $nobuildcodecheck;
my $oneshot;
my $silent;
my $hostcheck;

my $buildlog_maxsize = 500 * 1000000;
my $buildlog_maxidle = 8 * 3600;
my $xenstore_maxsize = 20 * 1000000;

my %cando = (
  'armv4l'  => [                                            'armv4l'                              ],
  'armv5el' => [                                            'armv4l', 'armv5el'                   ],
  'armv7el' => [                                            'armv4l', 'armv5el', 'armv7el'        ],
  'sh4'     => [                                                                            'sh4' ],
  'i586'    => [          'i586',                           'armv4l', 'armv5el', 'armv7el', 'sh4' ],
  'i686'    => [          'i586',         'i686',           'armv4l', 'armv5el', 'armv7el', 'sh4' ],
  'x86_64'  => ['x86_64', 'i586:linux32', 'i686:linux32',   'armv4l', 'armv5el', 'armv7el', 'sh4' ],
  'ppc'     => ['ppc'],
  'ppc64'   => ['ppc64', 'ppc:powerpc32'],
  'ia64'    => ['ia64'],
  's390'    => ['s390'],
  's390x'   => ['s390x', 's390:s390'],
  'sparc'   => ['sparc'],
  'sparc64' => ['sparc64', 'sparc:sparc32'],
  'mips'    => ['mips'],
  'mips64'  => ['mips64', 'mips:mips32'],
);

sub lockstate {
  while (1) {
    open(STATELOCK, '>>', "$statedir/state") || die("$statedir/state: $!\n");
    flock(STATELOCK, LOCK_EX) || die("flock $statedir/state: $!\n");
    my @s = stat(STATELOCK);
    last if $s[3];	# check nlink
    close(STATELOCK);	# race, try again
  }
  my $oldstate = readxml("$statedir/state", $BSXML::workerstate, 1);
  $oldstate = {} unless $oldstate;
  return $oldstate;
}

sub unlockstate {
  close(STATELOCK);
}

sub commitstate {
  my ($newstate) = @_;
  writexml("$statedir/state.new", "$statedir/state", $newstate, $BSXML::workerstate) if $newstate;
  close(STATELOCK);
}

sub trunc_logfile {
  my $lf = shift;
  open(LF, "<$lf") || return; 
  my $buf;
  sysread(LF, $buf, 1000000);
  $buf .= "\n\n[truncated]\n\n";
  sysseek(LF, -1000000, 2);
  sysread(LF, $buf, 1000000, length($buf));
  close LF;
  $buf .= "\nLogfile got too big, killed job.\n";
  open(LF, ">$lf.new") || return; 
  syswrite(LF, $buf);
  close LF;
  rename("$lf.new", $lf);
}

sub usage {
  my ($ret) = @_;

print <<EOF;
Usage: $0 [OPTION] --root <directory> --statedir <directory>

       --root      : buildroot directory

       --port      : fixed port number

       --statedir  : state directory

       --id        : worker id

       --srcserver : define source server to be used

       --reposerver: define reposerver, can be used multiple times

       --arch      : define hostarch (overrides 'uname -m')
                     currently supported architectures: 
                     @{[sort keys %cando]}

       --xen       : enable xen

       --xendevice: set xen root device (default is <root>/root file)

       --xenswap  : set xen swap device (default is <root>/swap file)

       --test      : enable test mode

       --build     : just build the package, don't send anything back
                     (needs a buildinfo file as argument)

       --noworkerupdate
                   : do not check if the worker is up-to-date

       --nobuildupdate
                   : do not check if the build code is up-to-date

       --nocodeupdate
                   : do not update both worker and build code

       --oneshot <seconds>
                   : just build one package, do not wait more then
                     <seconds> seconds if nothing is available

       --hostcheck <hostcheck>
                   : call to check if the host can build the package

       --help      : this message

EOF
  exit $ret || 0;
}

my @saveargv = @ARGV;	# so we can restart ourself
my $justbuild;

exit(0) if @ARGV == 1 && $ARGV[0] eq '--selftest';

while (@ARGV) {
  usage(0) if $ARGV[0] eq '--help';
  if ($ARGV[0] eq '--root') {
    shift @ARGV;
    $buildroot = shift @ARGV;
    next;
  }
  if ($ARGV[0] eq '--port') {
    shift @ARGV;
    $port = shift @ARGV;
    next;
  }
  if ($ARGV[0] eq '--arch') {
    shift @ARGV;
    $hostarch = shift @ARGV;
    next;
  }
  if ($ARGV[0] eq '--statedir') {
    shift @ARGV;
    $statedir = shift @ARGV;
    next;
  }
  if ($ARGV[0] eq '--srcserver') {
    shift @ARGV;
    $srcserver = shift @ARGV;
    next;
  }
  if ($ARGV[0] eq '--reposerver') {
    shift @ARGV;
    my $server = shift @ARGV;
    push @reposervers, $server;
    next;
  }
  if ($ARGV[0] eq '--id') {
    shift @ARGV;
    $workerid = shift @ARGV;
    next;
  }
  if ($ARGV[0] eq '--test') {
    shift @ARGV;
    $testmode = 1;
    next;
  }
  if ($ARGV[0] eq '--xen') {
    $xen = ' --xen';
    shift @ARGV;
    next;
  }
  if ($ARGV[0] eq '--xendevice') {
    shift @ARGV;
    $xen_root = shift @ARGV;
    next;
  }
  if ($ARGV[0] eq '--xenswap') {
    shift @ARGV;
    $xen_swap = shift @ARGV;
    next;
  }
  if ($ARGV[0] eq '--build') {
    shift @ARGV;
    $justbuild = 1;
    next;
  }
  if ($ARGV[0] eq '--oneshot') {
    shift @ARGV;
    $oneshot = shift @ARGV;
    next;
  }
  if ($ARGV[0] eq '--hostcheck') {
    shift @ARGV;
    $hostcheck = shift @ARGV;
    next;
  }
  if ($ARGV[0] eq '--nocodeupdate') {
    shift @ARGV;
    $noworkercheck = 1;
    $nobuildcodecheck = 1;
    next;
  }
  if ($ARGV[0] eq '--noworkerupdate') {
    shift @ARGV;
    $noworkercheck = 1;
    next;
  }
  if ($ARGV[0] eq '--nobuildupdate') {
    shift @ARGV;
    $nobuildcodecheck = 1;
    next;
  }
  if ($ARGV[0] eq '--silent') {
    shift @ARGV;
    $silent= 1;
    next;
  }
  last;
}

usage(1) unless $buildroot && $statedir;

$xen_root = "$buildroot/root" unless $xen_root;
$xen_swap = "$buildroot/swap" unless $xen_swap;

# here's the build code we want to use
$::ENV{'BUILD_DIR'} = "$statedir/build";

if (!$hostarch) {
  $hostarch = `uname -m`;
  chomp $hostarch;
  die("could not determine hostarch\n") unless $hostarch;
}

die("arch $hostarch cannot build anything!\n") unless $cando{$hostarch};

$srcserver = $BSConfig::srcserver unless defined $srcserver;
@reposervers = @BSConfig::reposervers unless @reposervers;

if ($justbuild) {
  my $buildinfo = readxml($ARGV[0], $BSXML::buildinfo);
  $| = 1;
  dobuild($buildinfo);
  exit(0);
}

sub stream_logfile {
  my ($nostream, $start, $end) = @_;
  open(F, "<$buildroot/.build.log") || die("$buildroot/.build.log: $!\n");
  my @s = stat(F);
  $start ||= 0;
  if (defined($end)) {
    $end -= $start;
    die("end is smaller than start\n") if $end < 0;
  }
  die("Logfile is not that big\n") if $s[7] < $start;
  defined(sysseek(F, $start, 0)) || die("sysseek: $!\n");

  BSServer::reply(undef, 'Content-Type: application/octet-stream', 'Transfer-Encoding: chunked');
  my $pos = $start;
  while(!defined($end) || $end) {
    @s = stat(F);
    if ($s[7] <= $pos) {
      last if !$s[3];
      select(undef, undef, undef, .5);
      next;
    }
    my $data = '';
    my $l = $s[7] - $pos;
    $l = 4096 if $l > 4096;
    sysread(F, $data, $l);
    next unless length($data);
    $data = substr($data, 0, $end) if defined($end) && length($data) > $end;
    $pos += length($data);
    $end -= length($data) if defined $end;
    $data = sprintf("%X\r\n", length($data)).$data."\r\n";
    BSServer::swrite($data);
    last if $nostream && $pos >= $s[7];
  }
  close F;
  BSServer::swrite("0\r\n\r\n");
}

sub send_state {
  my ($state, $p, $ba, $exclude) = @_;
  my @args = ("state=$state", "arch=$ba", "port=$p");
  push @args, "workerid=$workerid" if defined $workerid;
  for my $server (@reposervers) {
    next if $exclude && $server eq $exclude;
    eval {
      BSRPC::rpc({
        'uri' => "$server/worker",
	'timeout' => 3,
      }, undef, @args);
    };
    print "send_state $server: $@" if $@;
  }
}

sub codemd5 {
  my ($dir) = @_;
  my @files = ls($dir);
  my $md5 = '';
  for my $file (sort @files) {
    next if -l "$dir/$file" || -d "$dir/$file";
    $md5 .= Digest::MD5::md5_hex(readstr("$dir/$file"))."  $file\n";
  }
  $md5 = Digest::MD5::md5_hex($md5);
  return $md5;
}

sub getcode {
  my ($dir, $uri, $ineval) = @_;

  # evalize ourself
  if (!$ineval) {
    my $md5;
    eval {
     $md5 = getcode($dir, $uri, 1);
    };
    if ($@) {
      warn($@);
      return '';
    }
    return $md5;
  }

  my $ndir = "$dir.new";
  my $odir = "$dir.old";

  # clean up stale runs
  if (-e $ndir) {
    unlink("$ndir/$_") for ls($ndir);
    rmdir($ndir) || die("rmdir $ndir: $!\n");
  }
  if (-e $odir) {
    unlink("$odir/$_") for ls($odir);
    rmdir($odir) || die("rmdir $odir: $!\n");
  }

  mkdir($ndir) || die("mkdir $ndir: $!\n");
  my $res = BSRPC::rpc({
    uri => $uri,
    directory => $ndir,
    withmd5 => 1,
    'receiver:application/x-cpio' => \&BSHTTP::cpio_receiver,
  });
  die("getcode error\n") unless $res;

  # got everything, clean things up, check if it really works
  if ($dir eq 'worker') {
    symlink('.', "$ndir/XML") || die("symlink: $!\n");
    chmod(0755, "$ndir/bs_worker");
    die("bs_worker selftest failed\n") if system("cd $ndir && ./bs_worker --selftest");
  } elsif ($dir eq 'build') {
    symlink('.', "$ndir/Build") || die("symlink: $!\n");
    symlink('.', "$ndir/Date") || die("symlink: $!\n");
    symlink('.', "$ndir/Time") || die("symlink: $!\n");
    # we just change everyfile to be on the safe side
    chmod(0755, "$ndir/$_->{'name'}") for @$res;
  }

  # ok, commit
  if (-e $dir) {
    rename($dir, $odir) || die("rename $dir $odir: $!\n");
  }
  rename($ndir, $dir) || die("rename $ndir $dir: $!\n");
  if (-e $odir) {
    unlink("$odir/$_") for ls($odir);
    rmdir($odir);
  }
  my $md5 = '';
  for my $file (sort {$a->{'name'} cmp $b->{'name'}} @$res) {
    $md5 .= "$file->{'md5'}  $file->{'name'}\n";
  }
  $md5 = Digest::MD5::md5_hex($md5);
  return $md5;
}


sub rm_rf {
  my ($dir) = @_;
  for my $f (ls($dir)) {
    if (! -l "$dir/$f" && -d _) {
      rm_rf("$dir/$f");
    } else {
      unlink("$dir/$f");
    }
  }
  rmdir($dir);
}

sub getsources {
  my ($buildinfo, $dir) = @_;

  my @meta;
  push @meta, "$buildinfo->{'srcmd5'}  $buildinfo->{'package'}";

  my $repo = $buildinfo->{'path'}->[0];
  my $projid = $repo->{'project'};
  my $res = BSRPC::rpc({
    uri => "$srcserver/getsources",
    directory => $dir,
    withmd5 => 1,
    'receiver:application/x-cpio' => \&BSHTTP::cpio_receiver,
  }, undef, "project=$projid", "package=$buildinfo->{'package'}", "srcmd5=$buildinfo->{'srcmd5'}");
  die("Error\n") unless ref($res) eq 'ARRAY';
  if (-e "$dir/.errors") {
    my $errors = readstr("$dir/.errors", 1);
    die("getsources: $errors");
  }
  # verify sources
  my %res = map {$_->{'name'} => $_} @$res;
  my $md5 = '';
  my @f = ls($dir);
  for my $f (sort @f) {
    die("unexpected file: $f") unless $res{$f};
    $md5 .= "$res{$f}->{'md5'}  $f\n";
  }
  $md5 = Digest::MD5::md5_hex($md5);
  die("source verification fails: $md5 != $buildinfo->{'verifymd5'}\n") if $md5 ne $buildinfo->{'verifymd5'};

  return @meta unless $buildinfo->{'file'} =~ /\.kiwi$/;

  # get additional kiwi sources
  my @sdep = grep {($_->{'repoarch'} || '') eq 'src'} @{$buildinfo->{'bdep'} || []};
  for my $src (@sdep) {
    print "$src->{'name'}, ";
    my $idir = "$src->{'project'}/$src->{'package'}";
    $idir = "$dir/images/$idir";
    mkdir_p($idir);
    my $res = BSRPC::rpc({
      uri => "$srcserver/getsources",
      directory => $idir,
      withmd5 => 1,
      'receiver:application/x-cpio' => \&BSHTTP::cpio_receiver,
    }, undef, "project=$src->{'project'}", "package=$src->{'package'}", "srcmd5=$src->{'srcmd5'}");
    die("Error\n") unless ref($res) eq 'ARRAY';
    if (-e "$idir/.errors") {
      my $errors = readstr("$idir/.errors", 1);
      die("getsources: $errors");
    }
    push @meta, "$src->{'srcmd5'}  $src->{'project'}/$src->{'package'}";
  }
  return @meta;
}

sub qsystem {
  my (@args) = @_;
  if ($silent) {
    my $pid;
    if (!($pid = xfork())) {
      open(STDOUT, ">/dev/null");
      exec(@args);
      die("$args[0]: $!\n"); 
    }
    waitpid($pid, 0) == $pid || die("waitpid $pid: $!\n"); 
    return $?;
  } else {
    return system @args;
  }
}

sub getbinaries_kiwiproduct {
  my ($buildinfo, $dir, $srcdir) = @_;

  # we need the Build package for queryhdrmd5
  if (!defined &Build::queryhdrmd5) {
    unshift @INC, "$statedir/build";
    require Build;
    Build->import();
  }

  # create list of prpaps
  my %prpaps;
  my %linkit;
  for my $dep (@{$buildinfo->{'bdep'} || []}) {
    next unless $dep->{'package'};
    my $repoarch = $dep->{'repoarch'} || $buildinfo->{'arch'};
    next if $repoarch eq 'src';
    $prpaps{"$dep->{'project'}/$dep->{'repository'}/$repoarch/$dep->{'package'}"} = 1;
    $linkit{"$dep->{'project'}/$dep->{'repository'}/$repoarch/$dep->{'package'}/$dep->{'name'}"} = 1 unless $dep->{'noinstall'};
  }

  # FIXME: multiple servers?
  my $repo = $buildinfo->{'path'}->[0];

  mkdir_p($dir);
  my %meta;
  for my $prpap (sort keys %prpaps) {
    my ($projid, $repoid, $arch, $packid) = split('/', $prpap, 4);
    my $prpdir = "$projid/$repoid";
    my $ddir = "$srcdir/repos/$prpdir";
    mkdir_p($ddir);
    my $res = BSRPC::rpc({
      'uri' => "$repo->{'server'}/build/$projid/$repoid/$arch/$packid",
      'directory' => $ddir,
      'map' => sub {
	my ($param, $name) = @_;
	if ($name =~ /-[^-]+-[^-]+\.([a-zA-Z][^\.\-]*)\.rpm$/) {
	  my $arch = $1;
	  mkdir_p("$param->{'directory'}/$arch");
	  $name = "$arch/$name";
	}
	return $name;
      },
      'receiver:application/x-cpio' => \&BSHTTP::cpio_receiver,
    }, undef, "view=cpio");
    die unless $res;
    my @todometa;
    my @todometa_f;
    for my $f (@$res) {
      my (undef, $name) = split('/', $f->{'name'}, 2);
      next unless defined($name) && $name =~ /^(.*)-[^-]+-[^-]+\.([a-zA-Z][^\.\-]*)\.rpm$/;
      my ($n, $rarch) = ($1, $2);
      my $id = Build::queryhdrmd5("$ddir/$f->{'name'}") || "deaddeaddeaddeaddeaddeaddeaddead";
      if ($linkit{"$prpap/$n"}) {
        link("$ddir/$f->{'name'}", "$dir/$n.rpm") unless -e "$dir/$n.rpm";
      }
      $meta{"$prpap/$n.$arch"} = $id;
    }
  }
  # create meta
  my @meta;
  for (sort keys %meta) {
    push @meta, "$meta{$_}  $_";
  } 
  return @meta;
}

sub getbinaries {
  my ($buildinfo, $dir, $srcdir) = @_;

  mkdir_p($dir);
  my $kiwimode = 1 if $buildinfo->{'file'} =~ /\.kiwi$/;

  if ($kiwimode && $buildinfo->{'imagetype'} && $buildinfo->{'imagetype'}->[0] eq 'product') {
    return getbinaries_kiwiproduct($buildinfo, $dir, $srcdir);
  }

  # we need the Build package for queryhdrmd5
  if (!defined &Build::queryhdrmd5) {
    unshift @INC, "$statedir/build";
    require Build;
    Build->import();
  }

  my @bdep = @{$buildinfo->{'bdep'} || []};
  my %bdep_noinstall = map {$_->{'name'} => 1} grep {$_->{'noinstall'} && ($_->{'repoarch'} || '') ne 'src'} @bdep;
  my %bdep_notmeta = map {$_->{'name'} => 1} grep {$_->{'notmeta'} && ($_->{'repoarch'} || '') ne 'src'} @bdep;
  @bdep = map {$_->{'name'}} grep {($_->{'repoarch'} || '') ne 'src'} @bdep;

  my %done;
  my @todo = @bdep;
  die("no binaries needed for this package?\n") unless @todo;
  my @meta;
  my %meta;
  my $projid = $buildinfo->{'path'}->[0]->{'project'};
  my $repoid = $buildinfo->{'path'}->[0]->{'repository'};

  for my $repo (@{$buildinfo->{'path'} || []}) {
    last if !@todo && !$kiwimode;
    my @args;
    my $ddir = $dir;
    if ($kiwimode) {
      my $prpdir = "$repo->{'project'}/$repo->{'repository'}";
      $ddir = "$srcdir/repos/$prpdir";
      # Always try to get all binaries, because kiwi needs to decide which to take
      push @args, "binaries=".join(',', @bdep);
    } else {
      $ddir = $dir;
      # get only missing packages
      push @args, "binaries=".join(',', @todo);
    }
    mkdir_p($ddir) || die("mkdir_p $ddir: $!\n");
    push @args, "project=$repo->{'project'}";
    push @args, "repository=$repo->{'repository'}";
    push @args, "arch=$buildinfo->{'arch'}";
    push @args, "nometa" if $kiwimode || $repo->{'project'} ne $projid || $repo->{'repository'} ne $repoid;
    my $res = BSRPC::rpc({
      'uri' => "$repo->{'server'}/getbinaries",
      'directory' => $ddir,
      'receiver:application/x-cpio' => \&BSHTTP::cpio_receiver,
    }, undef, @args);
    die("Error\n") unless ref($res) eq 'ARRAY';
    for (@$res) {
      $done{$1} = $_->{'name'} if $_->{'name'} =~ /^(.*)\.(?:rpm|deb)$/;
      $meta{$1} = 1 if $_->{'name'} =~ /^(.*)\.meta$/;
    }
    @todo = grep {!$done{$_}} @todo;
    if ($kiwimode) {
      my @m;
      for my $f (@$res) {
	next unless $f->{'name'} =~ /^(.*)\.(?:rpm|deb)$/;
	my $n = $1;
	if (!$bdep_notmeta{$n}) {
	  my $id = Build::queryhdrmd5("$ddir/$f->{'name'}") || "deaddeaddeaddeaddeaddeaddeaddead";
	  push @m, "$id  $repo->{'project'}/$repo->{'repository'}/$n";
	}
	if (!$bdep_noinstall{$n}) {
	  link("$ddir/$f->{'name'}", "$dir/$f->{'name'}") unless -e "$dir/$f->{'name'}";
	}
      }
      push @meta, sort {substr($a, 34) cmp substr($b, 34)} @m;
    }
  }
  die("getbinaries: missing packages: @todo\n") if @todo;

  if (!$kiwimode) {
    for my $dep (map {$_->{'name'}} grep {!$_->{'notmeta'}} @{$buildinfo->{'bdep'} || []}) {
      my $m;
      $m = readstr("$dir/$dep.meta", 1) if $meta{$dep};
      if (!$m) {
	my $id = Build::queryhdrmd5("$dir/$done{$dep}") || "deaddeaddeaddeaddeaddeaddeaddead";
	push @meta, "$id  $dep";
      } else {
	chomp $m;
	my @m = split("\n", $m);
	s/  /  $dep\// for @m;
	$m[0] =~ s/  .*/  $dep/;
	push @meta, @m;
      }
    }
    @meta = BSBuild::gen_meta("$buildinfo->{'srcmd5'}  $buildinfo->{'package'}", $buildinfo->{'subpack'} || [], @meta);
    shift @meta;	# strip srcinfo again
  }
  return @meta;
}

sub dobuild {
  my ($buildinfo) = @_;

  my $repo = $buildinfo->{'path'}->[0];
  my $arch = $buildinfo->{'arch'};
  my ($projid, $repoid) = ($repo->{'project'}, $repo->{'repository'});
  my $kiwimode = 1 if ($buildinfo->{'file'} =~ /\.kiwi$/);

  my $helper = '';
  /^\Q$arch\E:(.*)$/ && ($helper = $1) for @{$cando{$hostarch}};

  my @lt = localtime(time());
  my $timestring = sprintf "%04d-%02d-%02d %02d:%02d:%02d", $lt[5] + 1900, $lt[4] + 1, @lt[3,2,1,0];
  
  print "$timestring: building '$buildinfo->{'package'}' for project '$projid' repository '$repoid' arch '$arch'";
  print " using helper $helper" if $helper;
  print "\n";

  unlink("$buildroot/.build.meta");
  rm_rf("$buildroot/.build.packages") if -d "$buildroot/.build.packages";
  rm_rf("$buildroot/.build-srcdir") if -d "$buildroot/.build-srcdir";
  rm_rf("$buildroot/.pkgs") if -d "$buildroot/.pkgs";

  my $srcdir = "$buildroot/.build-srcdir";
  my $pkgdir = "$buildroot/.pkgs";

  my @meta;
  print "fetching sources, ";
  mkdir($srcdir) || die("mkdir $srcdir $!\n");
  push @meta, getsources($buildinfo, $srcdir);
  print "packages, ";
  push @meta, getbinaries($buildinfo, $pkgdir, $srcdir);

  writestr("$buildroot/.build.meta", undef, join("\n", @meta)."\n");

  my @configpath;
  if ($kiwimode) {
    @configpath = map {"path=$_->{'project'}/$_->{'repository'}"} @{$buildinfo->{'path'} || []};
    unshift @configpath, "path=$projid/$repoid" unless @configpath;
  }
  my $config = BSRPC::rpc("$srcserver/getconfig", undef, "project=$projid", "repository=$repoid", @configpath);
  writestr("$buildroot/.build.config", undef, $config);

  my $release = $buildinfo->{'release'};
  my $obsinstance = "";
  $obsinstance = $BSConfig::obsname if defined ($BSConfig::obsname);
  my $disturl = "obs://".$obsinstance."/".$buildinfo->{'project'}."/".$buildinfo->{'repository'}."/".$buildinfo->{'srcmd5'}."-".$buildinfo->{'package'};

  my @args;
  push @args, $helper if $helper;

  # build rpmlist for build script
  my @rpmlist;
  my @bdep = @{$buildinfo->{'bdep'} || []};
  if ($kiwimode) {
    @bdep = grep {($_->{'repoarch'} || '') ne 'src'} @bdep;
    @bdep = grep {!$_->{'noinstall'}} @bdep;
  }
  for my $bdep (map {$_->{'name'}} @bdep) {
    if (-e "$pkgdir/$bdep.rpm") {
      push @rpmlist, "$bdep $pkgdir/$bdep.rpm";
    } elsif (-e "$pkgdir/$bdep.deb") {
      push @rpmlist, "$bdep $pkgdir/$bdep.deb";
    } else {
      die("missing package: $bdep\n");
    }
  }
  push @rpmlist, "preinstall: ".join(' ', map {$_->{'name'}} grep {$_->{'preinstall'}} @bdep);
  push @rpmlist, "vminstall: ".join(' ', map {$_->{'name'}} grep {$_->{'vminstall'}} @bdep);
  push @rpmlist, "runscripts: ".join(' ', map {$_->{'name'}} grep {$_->{'runscripts'}} @bdep);
  writestr("$buildroot/.build.rpmlist", undef, join("\n", @rpmlist)."\n");

  print "building...\n";
  push @args, "$statedir/build/build";
  if ($xen) {
    mkdir("$buildroot/.mount") unless -d "$buildroot/.mount";
    push @args, '--root', "$buildroot/.mount";
    push @args, '--xen', "$xen_root";
    push @args, '--xenswap', "$xen_swap";
    my $xenmemory = readstr("$buildroot/memory", 1);
    push @args, '--xenmemory', $xenmemory if $xenmemory;
  } else {
    push @args, '--root', $buildroot;
  }
  push @args, '--clean';
  push @args, '--changelog';
  push @args, '--norootforbuild' unless $BSConfig::norootexceptions && $BSConfig::norootexceptions->{"$projid/$buildinfo->{'package'}"};
  push @args, '--baselibs-internal';
  push @args, '--lint';
  push @args, '--dist', "$buildroot/.build.config";
  push @args, '--rpmlist', "$buildroot/.build.rpmlist";
  push @args, '--logfile', "$buildroot/.build.log";
  push @args, '--release', "$release" if defined $release;
  push @args, '--debug' if $buildinfo->{'debuginfo'};
  push @args, '--arch', $arch;
  push @args, '--reason', "Building $buildinfo->{'package'} for project '$projid' repository '$repoid' arch '$arch' srcmd5 '$buildinfo->{'srcmd5'}'";
  push @args, '--disturl', $disturl;
  push @args, "$srcdir/$buildinfo->{'file'}";
  if (qsystem(@args)) {
    print "build failed\n";
    return 1;
  }
  if (! -s "$buildroot/.build.log") {
    print "build succeeded, but no logfile?\n";
    return 1;
  }

  if ($xen) {
    rm_rf("$buildroot/.build.packages");
    if (! -d "$buildroot/.mount/.build.packages") {
      # old style, call extractbuild
      print "extracting built packages...\n";
      @args = ();
      push @args, "$statedir/build/extractbuild";
      push @args, '--xenroot', "$xen_root";
      push @args, '--xenswap', "$xen_swap";
      if (system(@args)) {
        die("extractbuild failed\n");
      }
      mkdir_p("$buildroot/.build.packages");
      if (system("cd $buildroot/.build.packages && cpio --extract --no-absolute-filenames -v < $xen_swap")) {
	die("cpio extract failed\n");
      }
    } else {
      # new style, build already did extractbuild for us
      if(!rename("$buildroot/.mount/.build.packages", "$buildroot/.build.packages")) {
        print "final rename failed: $!";
        return 1;
      }
    }
    # XXX: extracted cpio is flat but code below expects those directories...
    symlink('.', "$buildroot/.build.packages/SRPMS");
    symlink('.', "$buildroot/.build.packages/DEBS");
    symlink('.', "$buildroot/.build.packages/KIWI");
  }
  print "build succeeded\n";
  return 0;
}


if (defined($oneshot)) {
  $oneshot = time() + $oneshot;
}

# better safe than sorry...
chdir($statedir) || die("$statedir: $!\n");


BSServer::deamonize(@ARGV) unless $oneshot;
$SIG{'PIPE'} = 'IGNORE' if $oneshot;

# calculate code meta md5
my $workercode = codemd5('worker');
my $buildcode = codemd5('build');
$| = 1;
print "starting worker $workercode build $buildcode\n";

# we always start idle
lockstate();
unlink("$statedir/job");
unlink("$buildroot/.build.log");
commitstate({'state' => 'idle'});

# start server process...
if ($port) {
  BSServer::serveropen($port);
} else {
  BSServer::serveropen(\$port);
}
mkdir($buildroot) unless -d $buildroot;
send_state('idle', $port, $hostarch);

my $idlecnt = 0;
my $rekillcnt = 0;

my $conf = {
  'timeout' => 10,
};
while (!BSServer::server($conf)) {
  # timeout handler, called every 10 seconds
  my $state = readxml("$statedir/state", $BSXML::workerstate, 1);
  next unless $state;

  if ($state->{'state'} eq 'idle') {
    if ($oneshot && time() > $oneshot) {
      send_state('exit', $port, $hostarch);
      print "exiting.\n";
      exit(0);
    }
    $idlecnt++;
    if ($idlecnt % 30 == 0) {
      # send idle message every 5 minutes in case the server was down
      $idlecnt = 0;
      send_state('idle', $port, $hostarch) if $state->{'state'} eq 'idle';
    }
  } else {
    $idlecnt = 0;
  }

  if ($state->{'state'} eq 'rebooting') {
    chdir("$statedir/worker") || die("$statedir/worker: $!");
    exec("./bs_worker", @saveargv);
    die("$statedir/worker/bs_worker: $!\n");	# oops
  }

  if ($state->{'state'} eq 'killed' || $state->{'state'} eq 'discarded') {
    $rekillcnt++;
    if ($rekillcnt % 12 == 0) {
      # re-kill after 2 minutes, maybe build is stuck somewhere
      $rekillcnt = 0;
      $state = lockstate();
      if ($state->{'state'} eq 'killed' || $state->{'state'} eq 'discarded') {
        if (system("$statedir/build/build", "--root", $buildroot, ($xen ? ('--xen', "$xen_root") : ()), "--kill")) {
	  warn("could not kill job\n");
        }
      }
      unlockstate();
    }
  } else {
    $rekillcnt = 0;
  }

  next unless $state->{'state'} eq 'building';

  my $locked = -1;
  while ($locked++ < 1) {
    $state = lockstate() if $locked == 1;
    last if $state->{'state'} ne 'building';
    my $ct = time();
    my @s = stat("$buildroot/.build.log");
    next unless @s;
    if ($s[7] > $buildlog_maxsize) {
      next unless $locked;
      if (system("$statedir/build/build", "--root", $buildroot, ($xen ? ('--xen', "$xen_root") : ()), "--kill")) {
	warn("could not kill job\n");
        last;
      }
      trunc_logfile("$buildroot/.build.log");
      $state->{'state'} = 'killed';
      commitstate($state);
      $locked = 0;
    } elsif ($ct - $s[9] > $buildlog_maxidle) {
      next unless $locked;
      if (system("$statedir/build/build", "--root", $buildroot, ($xen ? ('--xen', "$xen_root") : ()), "--kill")) {
	warn("could not kill job\n");
        last;
      }
      local *F;
      if (open(F, '>>', "$buildroot/.build.log")) {
	print F "\n\nJob seems to be stuck here, killed.\n";
	close F;
      }
      $state->{'state'} = 'killed';
      commitstate($state);
      $locked = 0;
    }
    last;
  }
  unlockstate() if $locked;
}

my $req = BSServer::readrequest();
my $path = $req->{'path'};
my $cgi = BSServer::parse_cgi($req);
if ($path eq '/info') {
  # check state?
  my $info = readstr("$statedir/job");
  BSServer::reply($info, 'Content-Type: text/xml');
  exit(0);
} elsif ($path eq '/logfile') {
  my $state = readxml("$statedir/state", $BSXML::workerstate, 1);
  die("not building\n") if $state->{'state'} ne 'building';
  if ($cgi->{'jobid'}) {
    my $infoxml = readstr('job');
    die("building a different job\n") unless $cgi->{'jobid'} eq Digest::MD5::md5_hex($infoxml);
  }
  stream_logfile($cgi->{'nostream'}, $cgi->{'start'}, $cgi->{'end'});
  exit(0);
} elsif ($path eq '/kill' || $path eq '/discard') {
  my $state = lockstate();
  die("not building\n") if $state->{'state'} ne 'building';
  if ($cgi->{'jobid'}) {
    my $infoxml = readstr('job');
    die("building a different job\n") unless $cgi->{'jobid'} eq Digest::MD5::md5_hex($infoxml);
  }
  if (system("$statedir/build/build", "--root", $buildroot, ($xen ? ('--xen', "$xen_root") : ()), "--kill")) {
    die("could not kill job\n");
  }
  local *F;
  if (open(F, '>>', "$buildroot/.build.log")) {
    if ($path eq '/kill') {
      print F "\n\nKilled Job\n";
    } else {
      print F "\n\nDiscarded Job\n";
    }
    close F;
  }
  if ($path eq '/kill') {
    $state->{'state'} = 'killed';
    commitstate($state);
    BSServer::reply("<status=\"ok\" />\n", 'Content-Type: text/xml');
  } else {
    $state->{'state'} = 'discarded';
    commitstate($state);
    BSServer::reply("<status=\"ok\" />\n", 'Content-Type: text/xml');
  }
  exit(0);
} elsif ($path ne '/build' || $req->{'action'} ne 'PUT') {
  die("unknown request: $path\n");
}

if ($xen && $xenstore_maxsize && 0 + (-s '/var/lib/xenstored/tdb') > $xenstore_maxsize) {
  die("xenstore too big:".(-s '/var/lib/xenstored/tdb')."\n");
}
my $state = lockstate();
if ($cgi->{'workercode'} && $cgi->{'port'} && $cgi->{'workercode'} ne $workercode && !$noworkercheck) {
  $state->{'state'} = 'rebooting';
  my $peer = "${BSServer::peer}:$cgi->{'port'}";
  $workercode = getcode('worker', "http://$peer/getworkercode");
  if (!$workercode) {
    $state->{'state'} = 'broken';	# eek
  } else {
    print "activating new worker code $workercode\n";
  }
  commitstate($state);
  die("rebooting...\n");
}

die("I am not idle!\n") unless $state->{'state'} eq 'idle';

BSServer::read_file('job.new');
my $infoxml = readstr('job.new');
die("bad job xml data\n") unless $infoxml =~ /<.*?>/s;
my $buildinfo = XMLin($BSXML::buildinfo, $infoxml);
my $jobid = $cgi->{'jobid'};
$jobid ||= Digest::MD5::md5_hex($infoxml);

$buildcode = codemd5('build');
if (!$nobuildcodecheck && $cgi->{'buildcode'} && $cgi->{'port'} && $cgi->{'buildcode'} ne $buildcode) {
  print "fetching new buildcode $cgi->{'buildcode'}, mine was $buildcode\n";
  my $peer = "${BSServer::peer}:$cgi->{'port'}";
  $buildcode = getcode('build', "http://$peer/getbuildcode");
  die("could not update build code\n") unless $buildcode;
}

rename('job.new', 'job') || die("rename job.new job: $!\n");

if ($hostcheck) {
  if (system($hostcheck, '--srcserver', $srcserver, "$statedir/job", 'precheck', $buildroot)) {
    unlink('job');
    die("400 cannot build this package\n");
  }
}

if ($testmode) {
  BSServer::reply("<status code=\"failed\">\n  <details>testmode activated</details>\n</status>\n", 'Status: 400 Testmode', 'Content-Type: text/xml');
} else {
  BSServer::reply("<status code=\"ok\">\n  <details>so much work, so little time...</details>\n</status>\n", 'Content-Type: text/xml');
}
print "got job, run build...\n";
unlink("$buildroot/.build.meta");
unlink("$buildroot/.build.packages");
unlink("$buildroot/.build.log");
writestr("$buildroot/.build.log", undef, '');

$state->{'state'} = 'building';
$state->{'jobid'} = $jobid;
commitstate($state);

my $repo = $buildinfo->{'path'}->[0];
send_state('building', $port, $hostarch, $repo->{'server'});

my $ex;
eval {
  $ex = dobuild($buildinfo);
};
if ($@) {
  local *F;
  if (open(F, '>>', "$buildroot/.build.log")) {
    print F $@;
    close(F);
  }
  print "$@";
  $ex = 1;
}

# build is done, send back result
$state = lockstate();

if ($state->{'state'} eq 'discarded') {
  # our poor job is no longer needed
  print "build discarded...\n";
  unlink("$buildroot/.build.log");
  unlink("$buildroot/job");
  $state = {'state' => 'idle'};
  commitstate($state);
  exit(0) if $oneshot && time() > $oneshot;
  send_state('idle', $port, $hostarch);
  exit(0);
}

if ($state->{'state'} ne 'building') {
  # something is wrong, consider job bad
  $ex = 1;
}

if (! -s "$buildroot/.build.log") {
  eval {
    if (defined($workerid)) {
      writestr("$buildroot/.build.log", undef, "build on $workerid did not create a logfile\n");
    } else {
      writestr("$buildroot/.build.log", undef, "build did not create a logfile\n");
    }
  };
  $ex = 1;
}

if ($hostcheck) {
  print "running post-build host check\n";
  if (system($hostcheck, '--srcserver', $srcserver, "$statedir/job", $ex ? 'failed' : 'succeeded', "$buildroot/.build.log")) {
    print "post-build host check failed\n";
    $ex = 2;
  }
}

my @send;
if ($ex == 0) {
  local *D;
  my @d;
  if (opendir(D, "$buildroot/.build.packages/RPMS")) {
    @d = map {"RPMS/$_"} grep {$_ ne '.' && $_ ne '..'} readdir(D);
    close D;
  }
  push @d, 'SRPMS';
  @d = ('DEBS') if $buildinfo->{'file'} =~ /\.dsc$/;
  @d = ('KIWI') if $buildinfo->{'file'} =~ /\.kiwi$/;
  for my $d (@d) {
    my @files = sort(ls("$buildroot/.build.packages/$d"));
    @files = grep {-f "$buildroot/.build.packages/$d/$_"} @files;
    push @send, map {"$buildroot/.build.packages/$d/$_"} @files;
  }
  @send = map {{name => (split('/', $_))[-1], filename => $_}} @send;
  if (!@send) {
    print "build did not create anything to send back!\n";
    $ex = 1;
  }
}
my $code;
if (!$ex) {
  print "build succeeded, send everything back...\n";
  $code = 'succeeded';
} elsif ($ex == 2) {
  print "build failed due to a bad host...\n";
  $code = 'badhost';
} else {
  print "build failed, send back logfile...\n";
  $code = 'failed';
}
push @send, {name => 'meta', filename => "$buildroot/.build.meta"} if -e "$buildroot/.build.meta";
push @send, {name => 'logfile', filename => "$buildroot/.build.log"};

if (!$testmode) {
  my $param = {
    uri => "$repo->{'server'}/putjob",
    request => 'POST',
    headers => [ 'Content-Type: application/x-cpio' ],
    chunked => 1,
    data => \&BSHTTP::cpio_sender,
    cpiofiles => \@send,
  };
  my @args = ("job=$buildinfo->{'job'}", "arch=$buildinfo->{'arch'}", "jobid=$jobid", "code=$code");
  if ($code eq 'badhost') {
    # don't transmit anything in the badhost case
    $param = {
      uri => "$repo->{'server'}/putjob",
      request => 'POST',
    };
  }
  eval {
    my $res = BSRPC::rpc($param, undef, @args);
  };
  if ($@) {
    print "rpc failed: $@\nsleeping one minute just in case...\n";
    sleep(60);
  } else {
    print "sent, all done...\n";
  }
} else {
  print "testmode, not sending anything\n";
  print Dumper(\@send);
}

unlink("$buildroot/.build.log");
unlink("$buildroot/job");
print "\n";

$state = {'state' => 'idle'};
commitstate($state);

exit(0) if $oneshot && time() > $oneshot;
send_state('idle', $port, $hostarch);

exit(0);
