#!/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 $buildlog_maxsize = 70 * 1000000;
my $buildlog_maxidle = 8 * 3600;
my $xenstore_maxsize = 20 * 1000000;

my %cando = (
  'i586'   => ['i586'],
  'i686'   => ['i586', 'i686'],
  'x86_64' => ['x86_64', 'i586:linux32', 'i686:linux32'],
  'ppc'    => ['ppc'],
  'ppc64'  => ['ppc64', 'ppc:powerpc32'],
  'sh4'    => ['sh4'],
);

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)

       --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;
  }
  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);
  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");
    # 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 $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 failes: $md5 != $buildinfo->{'verifymd5'}\n") if $md5 ne $buildinfo->{'verifymd5'};
}

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

  my @bdep = @{$buildinfo->{'bdep'} || []};
  @bdep = map {ref($_) ? $_->{'name'} || $_->{'_content'} : $_} @bdep;
  my %done;
  my @todo = @bdep;
  die("no binaries needed for this package?\n") unless @todo;
  my %meta = ();
  my $projid = $buildinfo->{'path'}->[0]->{'project'};
  my $repoid = $buildinfo->{'path'}->[0]->{'repository'};
  for my $repo (@{$buildinfo->{'path'} || []}) {
    last if !@todo;
    my @args;
    push @args, "project=$repo->{'project'}";
    push @args, "repository=$repo->{'repository'}";
    push @args, "arch=$buildinfo->{'arch'}";
    push @args, "binaries=".join(',', @todo);
    push @args, "nometa" if $repo->{'project'} ne $projid || $repo->{'repository'} ne $repoid;
    my $res = BSRPC::rpc({
      uri => "$repo->{'server'}/getbinaries",
      directory => $dir,
      '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;
  }
  die("getbinaries: missing packages: @todo\n") if @todo;
  my @missingmeta = grep {!$meta{$_}} @bdep;
  if (@missingmeta) {
    local *F;
    open(F, "| $statedir/build/getbinaryid --createmeta --manifest -") || die("getbinaryid: $!\n");
    print F join('', map {"$dir/$done{$_}\n"} @missingmeta);
    close(F) || die("getbinaryid failed: $?\n");
  }
}

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

  my $repo = $buildinfo->{'path'}->[0];
  my $arch = $buildinfo->{'arch'};
  my ($projid, $repoid) = ($repo->{'project'}, $repo->{'repository'});

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

  print "Building '$buildinfo->{'package'}' for project '$projid' repository '$repoid' arch '$arch'\n";
  print "using helper $helper\n" if $helper;

  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/.rpms") if -d "$buildroot/.rpms";
  rm_rf("$buildroot/kiwi-root") if -d "$buildroot/kiwi-root";
  print "fetching sources...\n";
  mkdir("$buildroot/.build-srcdir") || die("mkdir $buildroot/.build-srcdir: $!\n");
  getsources($buildinfo, "$buildroot/.build-srcdir");
  print "fetching packages...\n";
  mkdir("$buildroot/.rpms") || die("mkdir $buildroot/.rpms: $!\n");
  getbinaries($buildinfo, "$buildroot/.rpms");
  my @deps;
  for my $dep (map {$_->{'name'}} grep {!$_->{'notmeta'}} @{$buildinfo->{'bdep'} || []}) {
    my $m = readstr("$buildroot/.rpms/$dep.meta");
    chomp $m;
    my @m = split("\n", $m);
    s/  /  $dep\// for @m;
    $m[0] =~ s/  .*/  $dep/;
    push @deps, @m;
  }
  my @meta = BSBuild::gen_meta("$buildinfo->{'srcmd5'}  $buildinfo->{'package'}", $buildinfo->{'subpack'} || [], @deps);
  writestr("$buildroot/.build.meta", undef, join("\n", @meta)."\n");
  
  my $config = BSRPC::rpc("$srcserver/getconfig", undef, "project=$projid", "repository=$repoid");
  writestr("$buildroot/.config", undef, $config);

  my @rpmlist;
  my @bdep = @{$buildinfo->{'bdep'} || []};
  for my $bdep (map {$_->{'name'}} @bdep) {
    if (-e "$buildroot/.rpms/$bdep.rpm") {
      push @rpmlist, "$bdep $buildroot/.rpms/$bdep.rpm";
    } elsif (-e "$buildroot/.rpms/$bdep.deb") {
      push @rpmlist, "$bdep $buildroot/.rpms/$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/.rpmlist", undef, join("\n", @rpmlist)."\n");

  my $release = $buildinfo->{'release'};

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

  if ($buildinfo->{'file'} =~ /\.kiwi$/) {
    # will not be needed anymore with kiwi 2.36
    rename( "$buildroot/.build-srcdir/config.kiwi", "$buildroot/.build-srcdir/config.xml" );

    print "Build KIWI preparation directory\n";
    push @args, "kiwi";
    push @args, '--prepare', "$buildroot/.build-srcdir/";
    push @args, '--ignore-repos';
    push @args, '--add-repo', "$buildroot/.rpms/", "--add-repotype", "rpm-dir";
    push @args, '--logfile', "$buildroot/.build.log";
    push @args, '--root', "$buildroot/kiwi-root";
    if (system(@args)) {
      print "build failed\n";
      return 1;
    }

    # loop to create all images specified in kiwi file
    my @imagetypes = @{$buildinfo->{'imagetype'} || []};
    mkdir("$buildroot/.build.packages") || die("mkdir $buildroot/.build.packages");
    mkdir("$buildroot/.build.packages/KIWI") || die("mkdir $buildroot/.build.packages/KIWI");
    for my $itype (@imagetypes) {
      print "Build KIWI image: $itype\n";
      @args = ();
      push @args, $helper if $helper;
      push @args, "kiwi";
      push @args, "--create", "$buildroot/kiwi-root";
      push @args, '--ignore-repos';
      push @args, '--add-repo', "$buildroot/.rpms/", "--add-repotype", "rpm-dir";
      push @args, '--logfile', "$buildroot/.build.log.image";
      push @args, "-d", "$buildroot/.build.packages/KIWI";
      push @args, "--type", $itype;
      if (system(@args)) {
        print "build failed\n";
        return 1;
      }
      system( "cat", "$buildroot/.build.log.image", ">>", "$buildroot/.build.log" );
      unlink( "$buildroot/.build.log.image" );
    }
  } else {
    push @args, "$statedir/build/build";
    if ($xen) {
      mkdir("$buildroot/.mount") unless -d "$buildroot/.mount";
      push @args, '--root', "$buildroot/.mount";
      push @args, '--logfile', "$buildroot/.build.log";
      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';
    push @args, '--baselibs-internal';
    push @args, '--lint';
    push @args, '--dist', "$buildroot/.config";
    push @args, '--rpmlist', "$buildroot/.rpmlist";
    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, "$buildroot/.build-srcdir/$buildinfo->{'file'}";
    if (system(@args)) {
      print "build failed\n";
      return 1;
    }
  };
  if (! -s "$buildroot/.build.log") {
    print "build succeeded, but no logfile?\n";
    return 1;
  }

  print "build succeeded\n";
  if ($xen) {
    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");
    }
    # FIXME stream directly from swap
    mkdir_p("$buildroot/.build.packages");
    rm_rf("$buildroot/.build.packages/SRPMS");
    rm_rf("$buildroot/.build.packages/DEPS");
    rm_rf("$buildroot/.build.packages/KIWI");
    symlink('.', "$buildroot/.build.packages/SRPMS");
    symlink('.', "$buildroot/.build.packages/DEBS");
    symlink('.', "$buildroot/.build.packages/KIWI");
    if (system("cd $buildroot/.build.packages && cpio --extract --no-absolute-filenames -v < $xen_swap")) {
      die("cpio extract failed\n");
    }
  }
  return 0;
}


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


BSServer::deamonize(@ARGV);

# calculate code meta md5
my $workercode = codemd5('worker');
my $buildcode = codemd5('build');
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') {
    $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) {
  $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 ($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 ($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.log");
unlink("$buildroot/.build.meta");
unlink("$buildroot/.build.packages");

$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;
  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);
  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") {
  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;
}

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) {
    if (opendir(D, "$buildroot/.build.packages/$d")) {
      push @send, map {"$buildroot/.build.packages/$d/$_"} grep {/\.(?:deb|rpm|iso)$/} readdir(D);
      close D;
    }
  }
  @send = map {{name => (split('/', $_))[-1], filename => $_}} @send;
}
if (@send) {
  print "build succeeded, send everything back...\n";
  push @send, {name => 'meta', filename => "$buildroot/.build.meta"};
} else {
  print "build failed, send back logfile...\n";
  push @send, {name => 'meta', filename => "$buildroot/.build.meta"} if -e "$buildroot/.build.meta";
}
push @send, {name => 'logfile', filename => "$buildroot/.build.log"};

if (!$testmode) {
  eval {
    my $res = BSRPC::rpc({
      uri => "$repo->{'server'}/putjob",
      request => 'POST',
      headers => [ 'Content-Type: application/x-cpio' ],
      chunked => 1,
      data => \&BSHTTP::cpio_sender,
      cpiofiles => \@send,
    }, undef, "job=$buildinfo->{'job'}", "arch=$buildinfo->{'arch'}", "jobid=$jobid");
  };
  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";
}

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

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

send_state('idle', $port, $hostarch);
exit(0);

