#!/usr/bin/python -t
# 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 Library General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
# Copyright 2005 Dan Williams <dcbw@redhat.com> and Red Hat, Inc.


import socket
import os
import shutil
import time
import sys
if sys.version_info[0:2] < (2,6):
    import sha
else:
    import hashlib
import string
import time
import fcntl
import urllib
import errno
import exceptions
import threading
import signal
from plague import ArchUtils
from plague import FileDownloader
from plague import AuthedXMLRPCServer
from plague import HTTPServer
from plague import daemonize
from plague import ExecUtils
from optparse import OptionParser

sys.path.append('/usr/share/plague/builder')

import Config

certs = {}
build_arches = []

def log(string):
    sys.stdout.write(string)
    sys.stdout.flush()


def get_hostname(cfg, bind_all):
    cfg_hostname = cfg.get_str("Network", "hostname")
    if cfg_hostname and len(cfg_hostname):
        return cfg_hostname
    elif bind_all:
        return ''
    return socket.gethostname()

def get_url_for_file(cfg, file_path):
    """ Return a URL pointing to a particular file in our work dir """

    # Ensure the file we're turning into a URL lives in our builder work dir
    work_dir = cfg.get_str("Directories", "builder_work_dir")
    if not file_path.startswith(work_dir):
        return None
    file_part = file_path[len(work_dir) + 1:]
    port = "%s" % cfg.get_int("Network", "fileserver_port")
    if cfg.get_bool("SSL", "use_ssl"):
        method = "https://"
    else:
        method = "http://"
    hostname = get_hostname(cfg, False)
    full_url = "%s%s:%s/%s" % (method, hostname, port, file_part)
    return urllib.quote(full_url)


class BuilderMock(threading.Thread):
    """puts things together for an arch - baseclass for handling builds for 
       other arches"""

    def __init__(self, controller, uniqid, target_cfg, buildarch, srpm_url):
        self._controller = controller
        self.buildarch = buildarch
        self._starttime = time.time()
        self._endtime = 0
        self._mockstarttime = 0
        self._uniqid = uniqid
        self._status = 'init'
        self._die = False
        self._repo_locked = True
        self._repo_wait_start = 0
        self._files = []
        self._childpid = 0
        self._target_cfg = target_cfg
        self._builder_cfg = target_cfg.parent_cfg()
        self._srpm_url = srpm_url
        self._srpm_tries = 0
        self._log_fd = None
        self._mock_config = None
        self._done_status = ''
        self._mock_log = None
        self.buildroot = self._target_cfg.mock_config()

        self._work_dir = self._builder_cfg.get_str("Directories", "builder_work_dir")
        self._result_dir = os.path.join(self._work_dir, self._uniqid, "result")
        if not os.path.exists(self._result_dir):
            os.makedirs(self._result_dir)

        self._state_file = os.path.join(self._result_dir,"state.log")

        logfile = os.path.join(self._result_dir, "job.log")
        self._log_fd = open(logfile, "w+")

        threading.Thread.__init__(self)

    def starttime(self):
        return self._starttime

    def endtime(self):
        return self._endtime

    def die(self):
        if self.is_done_status() or self._done_status == 'killed':
            return True
        self._die = True
        return True

    def _handle_death(self):
        self._die = False
        self._done_status = 'killed'

        # Don't try to kill a running cleanup process
        if self._status != 'cleanup':
            # Kill a running non-cleanup mock process, if any
            if self._childpid:
                child_pgroup = 0 - self._childpid
                self._log("Killing build process group of child pid %d...\n" % self._childpid)
                try:
                    # Kill all members of the child's process group
                    os.kill(child_pgroup, 9)
                except OSError, e:
                    self._log("ERROR: Couldn't kill child process group %d: %s\n" % (child_pgroup, e))
                else:
                    # Ensure child process is reaped
                    self._log("Waiting for child process group to exit...\n")
                    try:
                        (pid, status) = os.waitpid(child_pgroup, 0)
                    except OSError, e:
                        pass
                    self._log("Build processes exited.\n")
                self._childpid = 0

            # Start cleanup up the job
            self._start_cleanup()

    def _log(self, string):
        if string and self._log_fd:
            self._log_fd.write(string)
            self._log_fd.flush()
            os.fsync(self._log_fd.fileno())
            if self._builder_cfg.get_bool("General", "debug"):
                s = "%s: " % self._uniqid
                sys.stdout.write(s + string)
                sys.stdout.flush()

    def dl_callback(self, dl_status, cb_data, err_msg):
        url = cb_data
        if dl_status == 'done':
            self._status = 'downloaded'
            self._log("Retrieved %s.\n" % url)
        elif dl_status == 'failed':
            # If job was cancelled, just return
            if self.is_done_status():
                return

            # Retry up to 5 times
            self._srpm_tries = self._srpm_tries + 1
            if self._srpm_tries >= 5:
                self._status = 'failed'
                self._log("ERROR: Failed to retrieve %s.\n" % url)
            else:
                # retry the download
                self._status = 'init'
                self._log("ERROR: Failed to retrieve %s on attempt %d (%s).  Trying again...\n" % (url, self._srpm_tries, err_msg))

    def _copy_mock_output_to_log(self):
        if self._mock_log and os.path.exists(self._mock_log):
            ml = open(self._mock_log, "r")
            line = "foo"
            while len(line):
                line = ml.readline()
                if len(line):
                    self._log_fd.write(line)
            ml.close()
            os.remove(self._mock_log)
            self._mock_log = None

    def _start_build(self):
        self._log("Starting step 'building' with command:\n")
        if not os.path.exists(self._result_dir):
            os.makedirs(self._result_dir)
        if not os.path.exists(self._result_dir):
            os.makedirs(self._result_dir)

        # Set up build process arguments
        args = []
        builder_cmd = os.path.abspath(self._builder_cfg.get_str("General", "builder_cmd"))
        cmd = builder_cmd
        if self.arch_command and len(self.arch_command):
            arg_list = self.arch_command.split()
            for arg in arg_list:
                args.append(arg)
            cmd = os.path.abspath(arg_list[0])
        args.append(builder_cmd)
        args.append("rebuild")
        args.append("-r")
        args.append(self.buildroot)
        args.append("--arch")
        args.append(self.buildarch)
        args.append("--resultdir=%s" % self._result_dir)
        args.append("--uniqueext=%s" % self._uniqid)
        args.append(self._srpm_path)
        self._log("   %s\n" % string.join(args))

        self._mock_log = os.path.join(self._result_dir, "mock-output.log")
        self._childpid = ExecUtils.exec_with_redirect(cmd, args, None, self._mock_log, self._mock_log)
        self._mockstarttime = time.time()
        self._status = 'prepping'

    def _start_cleanup(self):
        self._log("Cleaning up the buildroot...\n")

        args = []
        builder_cmd = os.path.abspath(self._builder_cfg.get_str("General", "builder_cmd"))
        cmd = builder_cmd
        if self.arch_command and len(self.arch_command):
            arg_list = self.arch_command.split()
            for arg in arg_list:
                args.append(arg)
            cmd = os.path.abspath(arg_list[0])
        args.append(builder_cmd)
        args.append("clean")
        args.append("--uniqueext=%s" % self._uniqid)
        args.append("-r")
        args.append(self.buildroot)

        self._log("   %s\n" % string.join(args))
        self._childpid = ExecUtils.exec_with_redirect(cmd, args, None, None, None)
        self._status = 'cleanup'

    def _mock_is_prepping(self):
        mock_status = self._get_mock_status()
        if mock_status:
            prepstates = ['enabl','creating cache','unpacking cache','setup']
            for s in prepstates:
                if mock_status.startswith(s):
                    return True
        return False

    def _get_mock_status(self):
        mockstatusfile = self._state_file
        if not os.path.exists(mockstatusfile):
            return None

        f = open(mockstatusfile, "r")
        fcntl.fcntl(f.fileno(), fcntl.F_SETFL, os.O_NONBLOCK)
        
        while True:
            try:
                lines = f.readlines()
            except OSError, e:
                if e.errno == errno.EAGAIN:
                    try:
                        time.sleep(0.25)
                    except KeyboardInterrupt:
                        pass
                    continue
            else:
                if not len(lines):
                    continue
                break
        f.close()
        mockstat = None
        lastline = lines[len(lines)-1].lower()
        statedelim = "state changed:"
        stateidx = lastline.rfind(statedelim)
        if stateidx>0:
            mockstat = lastline[stateidx+len(statedelim):].lstrip()
            return mockstat
        else:
            return None

    def _read_mock_config(self):
        mockconfigfile = os.path.join(self._result_dir, 'mockconfig.log')
        if not os.path.exists(mockconfigfile):
            return None

        f = open(mockconfigfile, "r")
        contents = {}
        for line in f:
            (item, loc) = line.split('=')
            item = item.strip()
            loc = loc.strip()
            contents[item] = loc
        f.close()
        return contents

    def _status_init(self):
        self._log("Starting download of %s.\n" % self._srpm_url)
        self._status = 'downloading'
        target_dir = os.path.dirname(self._srpm_path)
        try:
            dl_thread = FileDownloader.FileDownloader(self.dl_callback, self._srpm_url, self._srpm_url,
                        target_dir, ['.src.rpm'], certs)
            dl_thread.start()
        except FileDownloader.FileNameException, e:
            self._status = 'failed'
            self._log("ERROR: Failed to begin SRPM download.  Error: '%s'   URL: %s\n" % (e, self._srpm_url))
            
    def _status_downloading(self):
        pass

    def _status_downloaded(self):
        # We can't start doing anything with yum until the build
        # server tells us the repo is unlocked.
        if not self._repo_locked:
            self._start_build()
        else:
            # Only show this message once
            if self._repo_wait_start <= 0:
                self._log("Waiting for repository to unlock before starting the build...\n")
                self._repo_wait_start = time.time()

            # Kill a job in 'downloaded' state after 30 minutes because
            # it's likely orphaned
            if time.time() > (self._repo_wait_start + (60 * 30)):
                self._log("Job waited too long for repo to unlock. Killing it...\n")
                self.die()

    def _watch_mock(self, good_exit, bad_exit):
        (aux_pid, status) = os.waitpid(self._childpid, os.WNOHANG)
        status = os.WEXITSTATUS(status)
        if aux_pid:
            self._childpid = 0
            if status == 0:
                self._done_status = good_exit
            elif status > 0:
                self._done_status = bad_exit

            self._copy_mock_output_to_log()
            self._start_cleanup()

    def _status_prepping(self):
        # We need to make sure that mock has dumped the status file withing a certain
        # amount of time, otherwise we can't tell what it's doing
        if not os.path.exists(self._state_file):
            # something is wrong if mock takes more than 15s to write the status file
            if time.time() > self._mockstarttime + 15:
                self._mockstarttime = 0
                self._log("ERROR: Timed out waiting for the mock status file!  %s\n" % self._state_file)
                self.die()
        else:
            if not self._mock_config and self._mock_is_prepping():
                self._mock_config = self._read_mock_config()
            if not self._mock_is_prepping():
                self._status = 'building'
                return
            self._watch_mock('done', 'failed')

    def _status_building(self):
        self._watch_mock('done', 'failed')

    def _status_cleanup(self):
        (aux_pid, status) = os.waitpid(self._childpid, os.WNOHANG)
        if aux_pid:
            self._childpid = 0
            # Mock exited
            if self._mock_config:
                if self._mock_config.has_key('rootdir'):
                    mock_root_dir = os.path.abspath(os.path.join(self._mock_config['rootdir'], "../"))
                    # Ensure we're actually deleteing the job's rootdir
                    if mock_root_dir.endswith(self._uniqid):
                        shutil.rmtree(mock_root_dir, ignore_errors=True)

                source_dir = os.path.abspath(os.path.join(self._mock_config['rootdir'], "../source"))
                # Ensure we're actually deleteing the job's sourcedir
                if source_dir.endswith(os.path.join(self._uniqid, "source")):
                    shutil.rmtree(source_dir, ignore_errors=True)

        # Ensure child process is reaped, if any
        if self._childpid:
            try:
                self._log("Waiting for child process %d to exit.\n" % self._childpid)
                (pid, status) = os.waitpid(self._childpid, 0)
            except OSError, e:
                self._childpid = 0
                pass

        self._copy_mock_output_to_log()

        self._files = self._find_files()
        self._status = self._done_status

    def _job_done(self):
        self._log("-----------------------\n")
        if self._status == 'done':
            self._log("Job completed successfully.\n")
        elif self._status == 'failed':
            self._log("Job failed due to build errors!  Please see build logs.\n")
        elif self._status == 'killed':
            self._log("Job failed because it was killed.\n")
        self._log("\n\n")

        if self._log_fd:
            self._log_fd.close()
            self._log_fd = None

    def run(self):
        # Print out a nice message at the start of the job
        target_dict = self._target_cfg.target_dict()
        target_str = "%s-%s-%s-%s" % (target_dict['distro'], target_dict['target'], target_dict['arch'], target_dict['repo'])
        self._log("""Starting job:
   Time: %s
   Target: %s
   UID: %s
   Architecture: %s
   SRPM: %s\n\n""" % (time.asctime(time.localtime(self._starttime)), target_str, self._uniqid, self.buildarch, self._srpm_url))

        try:
            srpm_filename = FileDownloader.get_base_filename_from_url(self._srpm_url, ['.src.rpm'])
            self._srpm_path = os.path.join(self._work_dir, self._uniqid, "source", srpm_filename)
        except FileDownloader.FileNameException, e:
            self._log("ERROR: SRPM file name was invalid.  Message: '%s'\n" % e)
            self._status = 'failed'

        # Main build job work loop
        while not self.is_done_status():
            if self._die:
                self._handle_death()

            # Execute operations for our current status
            try:
                func = getattr(self, "_status_%s" % self._status)
            except AttributeError:
                self._log("ERROR: internal builder inconsistency, didn't recognize status '%s'.\n" % self._status)
                self._status = 'failed'
            else:
                func()
            time.sleep(3)

        self._job_done()
        self._endtime = time.time()
        if self._childpid:
            self._log("ERROR: childpid was !NULL  (%d)" % self._childpid)
        self._controller.notify_job_done(self)

    def _find_files(self):
        # Grab the list of files in our job's result dir and URL encode them
        files_in_dir = os.listdir(self._result_dir)
        if 'repodata' in files_in_dir:
            files_in_dir.remove('repodata')
        file_list = []
        self._log("\n")
        self._log("Output File List:\n")
        self._log("-----------------\n")
        for f in files_in_dir:
            file_url = get_url_for_file(self._builder_cfg, os.path.join(self._result_dir, f))
            if file_url:
                file_list.append(file_url)
                self._log("  Output File: %s\n" % urllib.unquote(file_url))
            else:
                self._log("  Error: Couldn't get file URL for file %s\n" % f)
        self._log("-----------------\n")
        return file_list

    def status(self):
        return self._status

    def uniqid(self):
        return self._uniqid

    def files(self):
        return self._files

    def repo_unlocked(self):
        self._repo_locked = False

    def is_done_status(self):
        if (self._status is 'done') or (self._status is 'killed') or (self._status is 'failed'):
            return True
        return False


class InvalidTargetError(exceptions.Exception): pass

class i386Arch(BuilderMock):
    def __init__(self, controller, uniqid, target_cfg, buildarch, srpm_url):
        self.arch_command = '/usr/bin/setarch i686'
        BuilderMock.__init__(self, controller, uniqid, target_cfg, buildarch, srpm_url)

class x86_64Arch(BuilderMock):
    def __init__(self, controller, uniqid, target_cfg, buildarch, srpm_url):
        self.arch_command = ''
        BuilderMock.__init__(self, controller, uniqid, target_cfg, buildarch, srpm_url)

class PPCArch(BuilderMock):
    def __init__(self, controller, uniqid, target_cfg, buildarch, srpm_url):
        self.arch_command = '/usr/bin/setarch ppc32'
        BuilderMock.__init__(self, controller, uniqid, target_cfg, buildarch, srpm_url)

class PPC64Arch(BuilderMock):
    def __init__(self, controller, uniqid, target_cfg, buildarch, srpm_url):
        self.arch_command = ''
        BuilderMock.__init__(self, controller, uniqid, target_cfg, buildarch, srpm_url)

class SparcArch(BuilderMock):
    def __init__(self, controller, uniqid, target_cfg, buildarch, srpm_url):
        self.arch_command = '/usr/bin/sparc32'
        BuilderMock.__init__(self, controller, uniqid, target_cfg, buildarch, srpm_url)

class Sparc64Arch(BuilderMock):
    def __init__(self, controller, uniqid, target_cfg, buildarch, srpm_url):
        self.arch_command = '/usr/bin/sparc64'
        BuilderMock.__init__(self, controller, uniqid, target_cfg, buildarch, srpm_url)

# Keep this global scope, used in __main__
builder_dict = {'i386':     i386Arch,
                'i486':     i386Arch,
                'i586':     i386Arch,
                'i686':     i386Arch,
                'athlon':   i386Arch,
                'x86_64':   x86_64Arch,
                'amd64':    x86_64Arch,
                'ia32e':    x86_64Arch,
                'ppc':      PPCArch,
                'ppc32':    PPCArch,
                'ppc64':    PPC64Arch,
                'sparc':    SparcArch,
                'sparcv8':  SparcArch,
                'sparcv9':  SparcArch,
                'sparc64':  Sparc64Arch
               }


class XMLRPCBuilderServer:
    def __init__(self, cfg, max_jobs):
        self._all_jobs = {} # unique id => awclass instance
        self._building_jobs_lock = threading.Lock()
        self._building_jobs = []
        self._cfg = cfg
        self._max_jobs = max_jobs

    def _log(self, string):
        if self._cfg.get_bool("General", "debug"):
            print string

    def _generate_uniqid(self, target_str, srpm_url):
        if sys.version_info[0:2] < (2,6):
            sum = sha.new()
        else:
            sum = hashlib.sha1()
        sum.update('%d %s %s' % (time.time(), target_str, srpm_url))
        return sum.hexdigest()

    def _get_target_cfg(self, target_dict):
        target_cfg = None

        # First try to find a target for buildarch specifically
        try:
            target_cfg = self._cfg.get_target(target_dict)
        except Config.InvalidTargetException:
            pass

        if not target_cfg:
            # If that doesn't work, just get a target that can build the arch
            try:
                target_cfg = self._cfg.get_target(target_dict, True)
            except Config.InvalidTargetException:
                pass

        return target_cfg

    def _get_arch_builder_instance(self, uniqid, target_cfg, buildarch, srpm_url):
        """hand it an arch it hands you back the builder instance you need"""

        if buildarch != 'noarch' and not builder_dict.has_key(buildarch):
            return None

        builder = None
        if buildarch == 'noarch':
            builder = builder_dict[target_cfg.arches()[0]]
        else:
            if buildarch in target_cfg.arches():
                builder = builder_dict[buildarch]

        if builder:
            return builder(self, uniqid, target_cfg, buildarch, srpm_url)
        return None

    def start_new_job(self, target_dict, srpm_url):
        target_str = "%s-%s-%s-%s" % (target_dict['distro'], target_dict['target'], target_dict['arch'], target_dict['repo'])

        self._building_jobs_lock.acquire()
        num_building = len(self._building_jobs)
        self._building_jobs_lock.release()
        if num_building >= self._max_jobs:
            self._log("Tried to build '%s' on target %s when already building" \
                        " %d/%d jobs" % (srpm_url, target_str, num_building, self._max_jobs))
            return 0

        target_cfg = self._get_target_cfg(target_dict)
        if not target_cfg:
            self._log("Tried to build '%s' on target %s which isn't supported" % (srpm_url, target_str))
            return 0

        uniqid = self._generate_uniqid(target_str, srpm_url)
        job = self._get_arch_builder_instance(uniqid, target_cfg, target_dict['arch'], srpm_url)
        if job != None:
            self._all_jobs[uniqid] = job
            self._building_jobs_lock.acquire()
            self._building_jobs.append(job)
            self._building_jobs_lock.release()
            filename = os.path.basename(srpm_url)
            self._log("%s: started %s on %s arch %s at time %d" % (uniqid, filename,
                        target_str, target_dict['arch'], job.starttime()))
            job.start()
        else:
            self._log("%s: Failed request for %s on %s UNSUPPORTED arch %s" %
                        (uniqid, srpm_url, target_str, target_dict['arch'], cur_time))
            uniqid = 0

        return uniqid

    def notify_job_done(self, archjob):
        self._building_jobs_lock.acquire()
        if archjob in self._building_jobs:
            self._building_jobs.remove(archjob)
        self._building_jobs_lock.release()

    def die(self, uniqid):
        try:
            job = self._all_jobs[uniqid]
            job.die()
        except KeyError:
            pass
        return 0

    def files(self, uniqid):
        try:
            job = self._all_jobs[uniqid]
            return job.files()
        except KeyError:
            pass
        return []
    
    def repo_unlocked(self, uniqid):
        try:
            job = self._all_jobs[uniqid]
            job.repo_unlocked()
        except KeyError:
            pass
        return 0

    def building_jobs(self):
        jobs = {}
        self._building_jobs_lock.acquire()
        building = 0
        for job in self._building_jobs:
            jobs[job.uniqid()] = job.status()
            building = building + 1
        free = self._max_jobs - building
        self._building_jobs_lock.release()
        return (jobs, free)

    def num_slots(self):
        return self._max_jobs

    def job_status(self, uniqid):
        try:
            job = self._all_jobs[uniqid]
            return job.status()
        except KeyError:
            pass
        return ''

    def supported_targets(self):
        targets = []
        for t in self._cfg.targets():
            td = t.target_dict()
            td['supported_arches'] = t.arches()
            targets.append(td)
        return targets


def drop_privs(user):
    """
    We can't and shouldn't run mock as root, so we drop privs.
    We have to run the HTTP server as root though so it can chroot
    to the fileserver directory.
    """

    if os.getuid() != 0:
        return

    import pwd
    import grp

    eu = user
    try:
        uid = int(eu)
    except ValueError:
        try:
            pwrec = pwd.getpwnam(eu)
        except KeyError:
            print "Username '%s' does not exist." % eu
            return -1
        uid = pwrec[2]
    else:
        try:
            pwrec = pwd.getpwuid(uid)
        except KeyError:
            print "User ID %d doesn't exist." % uid
            return -1
    gid = pwrec[3]

    if uid == 0:
        print "You cannot use the superuser as the 'builder_user' option."
        return -1

    # Make ourself members of the mock group build_user's group
    try:
        mock_req = grp.getgrnam('mock')
    except KeyError:
        print "Mock group doesn't exist."
        return -1
    groups = [mock_req[2], gid]
    os.setgroups(groups)

    try:
        os.setgid(gid)
    except OSError:
        print "Could drop group privileges. Error: '%s'" % sys.exc_info()
        return -1

    os.setuid(uid)
    return 0


def determine_build_arches(cfg):
    """
    Attempt to autodetect what architectures this machine can build for,
    based on the kernel's uname.  If that fails, fall back to options in
    the config file.
    """

    machine_arch = os.uname()[4]
    arches = []
    try:
        arches = ArchUtils.supported_arches[machine_arch]
    except KeyError:
        print "Unknown machine type.  Please update plague's ArchUtils.py file."

    # Ok, grab from config file if we can't autodetermine
    if not len(arches):
        arches = cfg.get_list("General", "build_arches")

    for arch in arches:
        if not arch in builder_dict.keys():
            print "Unknown arch '%s' is not supported." % arch
            sys.exit(1)

    return arches


def determine_max_jobs(cfg):
    """ Simple max job calculator based on number of CPUs """

    import commands
    max_jobs = 1
    cmd = "/usr/bin/getconf _NPROCESSORS_ONLN"
    (s, o) = commands.getstatusoutput(cmd)
    if s == 0:
        try:
            max_jobs = int(o)
        except ValueError:
            pass
    return max_jobs


bserver = None

def exit_handler(signum, frame):
    global bserver
    log("Received SIGTERM, quitting...\n")
    bserver.stop()

def main():
    global bserver

    usage = "Usage: %s  [-p <pidfile>] [-l <logfile>] [-d] -c <configfile>" % sys.argv[0]
    parser = OptionParser(usage=usage)
    parser.add_option("-p", "--pidfile", default=None,
        help='file to write the PID to')
    parser.add_option("-l", "--logfile", default=None,
        help="location of file to write log output to")
    parser.add_option("-d", "--daemon", default=False, action="store_true",
        help="daemonize (i.e., detach from the terminal)")
    parser.add_option("-c", "--configfile", default=None,
        help="location of the builder config file")
    (opts, args) = parser.parse_args()

    if not opts.configfile:
        log("Must specify a config file.\n")
        sys.exit(1)

    # Load in the config
    cfg = Config.BuilderConfig(opts.configfile)
    hostname = get_hostname(cfg, False)

    if cfg.get_bool("SSL", "use_ssl"):
        key_file = os.path.join(cfg.get_str("SSL", "builder_key_and_cert_dir"), "%s.pem" % hostname)
        certs['key_and_cert'] = key_file
        certs['ca_cert'] = cfg.get_str("SSL", "ca_cert")
        certs['peer_ca_cert'] = certs['ca_cert']

    build_arches = determine_build_arches(cfg)
    if not len(build_arches):
        log("Cannot determine buildable arches for this builder.  Exiting...\n")
        sys.exit(1)

    cfg.load_target_configs(build_arches)
    if len(cfg.targets()) == 0:
        log("No useable mock buildroots configured.  Exiting...\n")
        sys.exit(1)

    if opts.daemon:
        ret=daemonize.createDaemon()
        if ret:
            log("Daemonizing failed!\n")
            sys.exit(2)

    if opts.pidfile:
        f = open(opts.pidfile, 'w', 1)
        f.write('%d\n' % os.getpid())
        f.flush()
        f.close()

    if opts.logfile:
        logf=open(opts.logfile, 'a')
        sys.stdout=logf
        sys.stderr=logf

    work_dir = cfg.get_str("Directories", "builder_work_dir")
    if not os.path.exists(work_dir) or not os.access(work_dir, os.R_OK):
        log("%s does not exist or is not readable.\n" % work_dir)
        os._exit(1)

    # Start up the HTTP server thread which the build server
    # pulls completed RPMs from
    hostname = get_hostname(cfg, True)
    port = cfg.get_int("Network", "fileserver_port")
    http_server = HTTPServer.PlgHTTPServerManager((hostname, port), work_dir, certs)
    http_server.start()

    # Stop running as root
    if drop_privs(cfg.get_str("General", "builder_user")) == -1:
        http_server.stop()
        try:
            time.sleep(1)
        except KeyboardInterrupt:
            pass
        os._exit(1)

    log("Binding to address '%s' with arches: [%s]\n" % (hostname, string.join(build_arches, ",")))
    xmlrpc_port = cfg.get_int("Network", "xmlrpc_port")
    try:
        if cfg.get_bool("SSL", "use_ssl") == True:
            bserver = AuthedXMLRPCServer.AuthedSSLXMLRPCServer((hostname, xmlrpc_port), None, certs)
        else:
            bserver = AuthedXMLRPCServer.AuthedXMLRPCServer((hostname, xmlrpc_port), None)
    except socket.error, e:
        if e[0] == 98:
            log("Error: couldn't bind to address '%s:%s'.  "  \
                       "Is the builder already running?\n" % (hostname, xmlrpc_port))
            os._exit(1)

    max_jobs = determine_max_jobs(cfg)
    bcs = XMLRPCBuilderServer(cfg, max_jobs)
    bserver.register_instance(bcs)

    # Set up our termination handler
    signal.signal(signal.SIGTERM, exit_handler)

    try:
        bserver.serve_forever()
    except KeyboardInterrupt, e:
        bserver.server_close()

    log("Shutting down...\n")
    http_server.stop()

    (building_jobs, free) = bcs.building_jobs()
    for jobid in building_jobs.keys():
        bcs.die(jobid)

    # wait for the jobs to clean up before quitting
    log("Waiting for running jobs to stop...")
    while True:
        (building_jobs, free) = bcs.building_jobs()
        if len(building_jobs.keys()) == 0:
            break
        try:
            log(".")
            time.sleep(0.5)
        except KeyboardInterrupt:
            break
    log(" done.\n");
    sys.stdout.flush()
    time.sleep(2)
    os._exit(0)


if __name__ == '__main__':
    main()
