#!/usr/bin/python

import sys
from retrace import *

sys.path.insert(0, "/usr/share/retrace-server/")
from plugins import *

starttime = int(time.time())
log = ""
task = None
stats = {
  "taskid": None,
  "package": None,
  "version": None,
  "arch": None,
  "starttime": starttime,
  "duration": None,
  "coresize": None,
  "status": STATUS_FAIL,
}

def fail(exitcode):
    "Kills script with given exitcode"
    global task, log
    task.set_status(STATUS_FAIL)
    task.set_log(log)
    stats["duration"] = int(time.time()) - stats["starttime"]
    save_crashstats(stats)
    if not task.get_type() in [TASK_DEBUG, TASK_RETRACE_INTERACTIVE, TASK_VMCORE_INTERACTIVE]:
        task.clean()
    sys.exit(exitcode)

def retrace_run(errorcode, cmd):
    "Runs cmd using subprocess.Popen and kills script with errorcode on failure"
    try:
        child = Popen(cmd, stdout=PIPE, stderr=STDOUT)
        output = child.communicate()[0]
    except Exception as ex:
        child = None
        output = "An unhandled exception occured: %s" % ex

    if not child or child.returncode != 0:
        global log
        log += "Error %d:\n=== OUTPUT ===\n%s\n" % (errorcode, output)
        fail(errorcode)

    return output

def start_retrace():
    global log
    crashdir = os.path.join(task.get_savedir(), "crash")
    corepath = os.path.join(crashdir, "coredump")
    try:
        stats["coresize"] = os.path.getsize(corepath)
    except:
        pass

    # read architecture from coredump
    arch = guess_arch(corepath)

    if not arch:
        log += "Error\nUnable to read architecture from 'coredump' file.\n"
        fail(16)

    stats["arch"] = arch

    # read package file
    try:
        with open(os.path.join(crashdir, "package"), "r") as package_file:
            crash_package = package_file.read(ALLOWED_FILES["package"])
    except Exception as ex:
        log += "Error\nUnable to read crash package from 'package' file: %s.\n" % ex
        fail(17)

    # read package file
    if not INPUT_PACKAGE_PARSER.match(crash_package):
        log += "Error\nInvalid package name: %s.\n" % crash_package
        fail(19)

    pkgdata = parse_rpm_name(crash_package)
    if not pkgdata["name"]:
        log += "Error\nUnable to parse package name: %s.\n" % crash_package
        fail(19)

    stats["package"] = pkgdata["name"]
    if pkgdata["epoch"] != 0:
        stats["version"] = "%s:%s-%s" % (pkgdata["epoch"], pkgdata["version"], pkgdata["release"])
    else:
        stats["version"] = "%s-%s" % (pkgdata["version"], pkgdata["release"])

    # read release, distribution and version from release file
    release_path = os.path.join(crashdir, "os_release")
    if not os.path.isfile(release_path):
        release_path = os.path.join(crashdir, "release")

    try:
        with open(release_path, "r") as release_file:
            release = release_file.read(ALLOWED_FILES["os_release"])

        version = distribution = None
        for plugin in PLUGINS:
            match = plugin.abrtparser.match(release)
            if match:
                version = match.group(1)
                distribution = plugin.distribution
                break

        if not version or not distribution:
            raise Exception, "Unknown release '%s'" % release

    except Exception as ex:
        log += "Error\nUnable to read distribution and version from 'release' file: %s.\n" % ex
        log += "Trying to guess distribution and version... "
        distribution, version = guess_release(crash_package, PLUGINS)
        if distribution and version:
            log += "%s-%s\n" % (distribution, version)
        else:
            log += "Failure\n"
            fail(18)

    if "rawhide" in release.lower():
        version = "rawhide"

    releaseid = "%s-%s-%s" % (distribution, version, arch)
    if not releaseid in get_supported_releases():
        log += "Error\nRelease '%s' is not supported.\n" % releaseid
        fail(19)

    packages = [crash_package]
    missing = []
    # read required packages from coredump
    try:
        repoid = "%s%s" % (REPO_PREFIX, releaseid)
        yumcfgpath = os.path.join(task.get_savedir(), "yum.conf")
        with open(yumcfgpath, "w") as yumcfg:
            yumcfg.write("[%s]\n" % repoid)
            yumcfg.write("name=%s\n" % releaseid)
            yumcfg.write("baseurl=file://%s/%s/\n" % (CONFIG["RepoDir"], releaseid))
            yumcfg.write("failovermethod=priority\n")

        child = Popen(["coredump2packages", os.path.join(crashdir, "coredump"),
                       "--repos=%s" % repoid, "--config=%s" % yumcfgpath],
                      stdout=PIPE)
        section = 0
        crash_package_or_component = None
        lines = child.communicate()[0].split("\n")
        libdb = False
        for line in lines:
            if line == "":
                section += 1
                continue
            elif 0 == section:
                crash_package_or_component = line.strip()
            elif 1 == section:
                stripped = line.strip()

                # hack - help to depsolver, yum would fail otherwise
                if distribution == "fedora" and stripped.startswith("gnome"):
                    packages.append("desktop-backgrounds-gnome")

                # hack - libdb-debuginfo and db4-debuginfo are conflicting
                if distribution == "fedora" and \
                   (stripped.startswith("db4-debuginfo") or \
                    stripped.startswith("libdb-debuginfo")):
                    if libdb:
                        continue
                    else:
                        libdb = True

                packages.append(stripped)
            elif 2 == section:
                soname, buildid = line.strip().split(" ", 1)
                if not soname or soname == "-":
                    soname = None
                missing.append((soname, buildid))

    except Exception as ex:
        log += "Error\nUnable to obtain packages from 'coredump' file: %s.\n" % ex
        fail(20)

    # create mock config file
    try:
        with open(os.path.join(task.get_savedir(), "default.cfg"), "w") as mockcfg:
            mockcfg.write("config_opts['root'] = '%s'\n" % taskid)
            mockcfg.write("config_opts['target_arch'] = '%s'\n" % arch)
            mockcfg.write("config_opts['chroot_setup_cmd'] = '--skip-broken install %s shadow-utils gdb rpm'\n" % " ".join(packages))
            mockcfg.write("config_opts['plugin_conf']['ccache_enable'] = False\n")
            mockcfg.write("config_opts['plugin_conf']['yum_cache_enable'] = False\n")
            mockcfg.write("config_opts['plugin_conf']['root_cache_enable'] = False\n")
            mockcfg.write("\n")
            mockcfg.write("config_opts['yum.conf'] = \"\"\"\n")
            mockcfg.write("[main]\n")
            mockcfg.write("cachedir=/var/cache/yum\n")
            mockcfg.write("debuglevel=1\n")
            mockcfg.write("reposdir=/dev/null\n")
            mockcfg.write("logfile=/var/log/yum.log\n")
            mockcfg.write("retries=20\n")
            mockcfg.write("obsoletes=1\n")
            if version != "rawhide" and CONFIG["RequireGPGCheck"]:
                mockcfg.write("gpgcheck=1\n")
            else:
                mockcfg.write("gpgcheck=0\n")
            mockcfg.write("assumeyes=1\n")
            mockcfg.write("syslog_ident=mock\n")
            mockcfg.write("syslog_device=\n")
            mockcfg.write("\n")
            mockcfg.write("#repos\n")
            mockcfg.write("\n")
            mockcfg.write("[%s]\n" % distribution)
            mockcfg.write("name=%s\n" % releaseid)
            mockcfg.write("baseurl=file://%s/%s/\n" % (CONFIG["RepoDir"], releaseid))
            mockcfg.write("failovermethod=priority\n")
            if version != "rawhide" and CONFIG["RequireGPGCheck"]:
                mockcfg.write("gpgkey=file:///usr/share/retrace-server/gpg/%s-%s\n" % (distribution, version))
            mockcfg.write("\"\"\"\n")

        # symlink defaults from /etc/mock
        os.symlink("/etc/mock/site-defaults.cfg", os.path.join(task.get_savedir(), "site-defaults.cfg"))
        os.symlink("/etc/mock/logging.ini", os.path.join(task.get_savedir(), "logging.ini"))
    except Exception as ex:
        log += "Error\nUnable to create mock config file: %s.\n" % ex
        fail(21)

    log += "OK\n"

    # run retrace
    task.set_status(STATUS_INIT)
    log += "%s " % STATUS[STATUS_INIT]

    retrace_run(25, ["/usr/bin/mock", "init", "--configdir", task.get_savedir()])
    retrace_run(26, ["/usr/bin/mock", "--configdir", task.get_savedir(), "--copyin",
                     crashdir, "/var/spool/abrt/crash"])
    retrace_run(27, ["/usr/bin/mock", "--configdir", task.get_savedir(), "shell",
                     "--", "chgrp", "-R", "mockbuild", "/var/spool/abrt/crash"])

    log += "OK\n"

    # generate backtrace
    task.set_status(STATUS_BACKTRACE)
    log += "%s " % STATUS[STATUS_BACKTRACE]

    try:
        backtrace = run_gdb(task.get_savedir())
    except Exception as ex:
        log += "Error\n%s\n" % ex
        fail(29)

    task.set_backtrace(backtrace)
    log += "OK\n"

    # does not work at the moment
    rootsize = 0

    if not task.get_type() in [TASK_DEBUG, TASK_RETRACE_INTERACTIVE]:
        # clean up temporary data
        task.set_status(STATUS_CLEANUP)
        log += "%s " % STATUS[STATUS_CLEANUP]

        task.clean()

        # ignore error: workdir = savedir => workdir is not empty
        if CONFIG["UseWorkDir"]:
            try:
                os.rmdir(workdir)
            except:
                pass

        log += "OK\n"

    # save crash statistics
    task.set_status(STATUS_STATS)
    log += "%s " % STATUS[STATUS_STATS]

    stats["duration"] = int(time.time()) - stats["starttime"]
    stats["status"] = STATUS_SUCCESS

    # publish log => finish task
    log += "Retrace took %d seconds.\n" % stats["duration"]
    log += "%s\n" % STATUS[STATUS_SUCCESS]

    task.set_log(log)
    task.set_status(STATUS_SUCCESS)

def find_kernel_debuginfo(kernelver):
    vers = [kernelver]
    if kernelver.endswith(".i386"):
        vers.append(kernelver.replace(".i386", ".i486"))
        vers.append(kernelver.replace(".i386", ".i586"))
        vers.append(kernelver.replace(".i386", ".i686"))

    # search for the debuginfo RPM
    for release in os.listdir(CONFIG["RepoDir"]):
        for ver in vers:
            testfile = os.path.join(CONFIG["RepoDir"], release, "Packages", "kernel-debuginfo-%s.rpm" % ver)
            if os.path.isfile(testfile):
                return testfile

            # should not happen, but anyway...
            testfile = os.path.join(CONFIG["RepoDir"], release, "kernel-debuginfo-%s.rpm" % ver)
            if os.path.isfile(testfile):
                return testfile

    return None

def cache_vmlinux_from_debuginfo(kernelver, kernelver_noarch=None):
    global log
    kernelcache = os.path.join(CONFIG["RepoDir"], "kernel")
    kerneltmp = os.path.join(kernelcache, "%s.tmp" % kernelver)

    rpmfile = find_kernel_debuginfo(kernelver)

    if not rpmfile:
        log += "Error\nUnable to find the debuginfo for kernel-%s\n" % kernelver
        shutil.rmtree(kerneltmp)
        fail(64)

    with open("/dev/null", "w") as null:
        rpm2cpio = Popen(["rpm2cpio", rpmfile], stdout=PIPE, stderr=null)
        cpio = Popen(["cpio", "-id"], stdin=rpm2cpio.stdout, stdout=null, stderr=null, cwd=kerneltmp)
        ret1 = rpm2cpio.wait()
        ret2 = cpio.wait()
        rpm2cpio.stdout.close()

        if ret1:
            log += "Warning: rpm2cpio exited with %d; " % ret1
        if ret2:
            log += "Warning: cpio exited with %d; " % ret2

    vmlinux = None
    cand = os.path.join(kerneltmp, "usr", "lib", "debug", "lib", "modules", kernelver, "vmlinux")
    if os.path.isfile(cand):
        vmlinux = cand
    elif kernelver_noarch:
        cand = os.path.join(kerneltmp, "usr", "lib", "debug", "lib", "modules", kernelver_noarch, "vmlinux")
        if os.path.isfile(cand):
            vmlinux = cand

    if not vmlinux:
        log += "Error\nUnable to find vmlinux\n"
        shutil.rmtree(kerneltmp)
        fail(67)

    vmlinux_cache = os.path.join(kernelcache, "vmlinux-%s" % kernelver)
    try:
        shutil.copy(vmlinux, vmlinux_cache)
    except Exception as ex:
        log += "Error\nUnable to cache vmlinux: %s\n" % ex
        shutil.rmtree(kerneltmp)
        fail(68)

    shutil.rmtree(kerneltmp)

def mock_find_vmlinux(cfgdir, candidates):
    with open("/dev/null", "w") as null:
        for cand in candidates:
            child = Popen(["/usr/bin/mock", "--configdir", cfgdir, "shell", "--",
                           "test", "-f", cand, "&&", "echo", cand], stdout=PIPE, stderr=null)
            output = child.communicate()[0].strip()
            child.wait()
            if output == cand:
                return cand

    return None

def start_vmcore():
    global log
    vmcore = os.path.join(task.get_savedir(), "crash", "vmcore")

    kernelver = get_kernel_release(vmcore)
    if not kernelver:
        log += "Error\nUnable to determine kernel version\n"
        fail(50)

    match = KERNEL_RELEASE_PARSER.match(kernelver)
    if not match:
        log += "Error\nUnable to parse kernel version\n"
        fail(51)

    kernelver_noarch = match.group(1)
    arch = match.group(4)
    if arch in ["i486", "i586", "i686"]:
        arch = "i386"

    stats["package"] = "kernel"
    stats["version"] = kernelver_noarch
    stats["arch"] = arch

    kernelcache = os.path.join(CONFIG["RepoDir"], "kernel")
    kerneltmp = os.path.join(kernelcache, "%s.tmp" % kernelver)
    hostarch = os.uname()[4]
    if hostarch in ["i486", "i586", "i686"]:
        hostarch = "i386"

    vmlinux = os.path.join(kernelcache, "vmlinux-%s" % kernelver)

    log += "OK\n%s " % STATUS[STATUS_INIT]
    task.set_status(STATUS_INIT)

    # cross-arch: we need to use chroot
    if hostarch != arch:
        cfgdir = os.path.join(CONFIG["SaveDir"], "kernel-%s" % arch)
        tmpdir = os.path.join(CONFIG["SaveDir"], "kernel-%s.tmp" % arch)
        wait = False
        if not os.path.isdir(cfgdir):
            try:
                os.mkdir(tmpdir)
            except OSError as ex:
                if ex[0] == errno.EEXIST:
                    wait = True
                else:
                    raise ex

            # no exception - we are the one who prepares the chroot
            if not wait:
                # create mock config file
                try:
                    with open(os.path.join(tmpdir, "default.cfg"), "w") as mockcfg:
                        mockcfg.write("config_opts['root'] = 'kernel-%s'\n" % arch)
                        mockcfg.write("config_opts['target_arch'] = '%s'\n" % arch)
                        mockcfg.write("config_opts['chroot_setup_cmd'] = 'install bash coreutils cpio crash rpm shadow-utils'\n")
                        mockcfg.write("config_opts['plugin_conf']['ccache_enable'] = False\n")
                        mockcfg.write("config_opts['plugin_conf']['yum_cache_enable'] = False\n")
                        mockcfg.write("config_opts['plugin_conf']['root_cache_enable'] = False\n")
                        mockcfg.write("\n")
                        mockcfg.write("config_opts['yum.conf'] = \"\"\"\n")
                        mockcfg.write("[main]\n")
                        mockcfg.write("cachedir=/var/cache/yum\n")
                        mockcfg.write("debuglevel=1\n")
                        mockcfg.write("reposdir=/dev/null\n")
                        mockcfg.write("logfile=/var/log/yum.log\n")
                        mockcfg.write("retries=20\n")
                        mockcfg.write("obsoletes=1\n")
                        mockcfg.write("assumeyes=1\n")
                        mockcfg.write("syslog_ident=mock\n")
                        mockcfg.write("syslog_device=\n")
                        mockcfg.write("\n")
                        mockcfg.write("#repos\n")
                        mockcfg.write("\n")
                        mockcfg.write("[kernel-%s]\n" % arch)
                        mockcfg.write("name=kernel-%s\n" % arch)
                        mockcfg.write("baseurl=%s\n" % CONFIG["KernelChrootRepo"].replace("$ARCH", arch))
                        mockcfg.write("failovermethod=priority\n")
                        mockcfg.write("\"\"\"\n")

                    # symlink defaults from /etc/mock
                    os.symlink("/etc/mock/site-defaults.cfg", os.path.join(tmpdir, "site-defaults.cfg"))
                    os.symlink("/etc/mock/logging.ini", os.path.join(tmpdir, "logging.ini"))
                except Exception as ex:
                    log += "Error\nUnable to create mock config file: %s.\n" % ex
                    fail(52)

                retrace_run(53, ["/usr/bin/mock", "--configdir", tmpdir, "init"])

                try:
                    os.rename(tmpdir, cfgdir)
                except OSError as ex:
                    if ex[0] == errno.EEXIST:
                        pass
                    else:
                        raise ex

        # someone else is preparing the chroot, wait for him to finish
        if wait:
            i = 0
            while i < 1800 and not os.path.isdir(cfgdir):
                i += 1
                time.sleep(1)

            # error
            if i >= 1800:
                log += "Error\nWaiting for chroot timed out\n"
                fail(54)

        lockfile = os.path.join(cfgdir, ".lock")
        i = 0
        while i < 1800 and not lock(lockfile):
            i += 1
            time.sleep(1)

        # error
        if i >= 1800:
            log += "Error\nWaiting for chroot lock timed out\n"
            fail(55)

        # we have the lock, let's work
        try:
            # we need to unpack the debuginfo
            if not os.path.isfile(vmlinux):
                wait = False
                try:
                    os.mkdir(kerneltmp)
                except OSError as ex:
                    if ex[0] == errno.EEXIST:
                        wait = True
                    else:
                        raise ex

                if not wait:
                    cache_vmlinux_from_debuginfo(kernelver, kernelver_noarch)

                if wait:
                    i = 0
                    while i < 1800 and not os.path.isfile(vmlinux):
                        i += 1
                        time.sleep(1)

                    if i >= 1800:
                        log += "Error\nUnpacking kernel debuginfo timed out\n"
                        fail(56)

            # generate the log
            retrace_run(59, ["/usr/bin/mock", "--configdir", cfgdir, "shell", "--", "mkdir", "-p", "/var/crash/"])
            retrace_run(60, ["/usr/bin/mock", "--configdir", cfgdir, "--copyin", vmcore, "/var/crash/vmcore"])
            with open("/dev/null", "w") as null:
                retrace_run(57, ["/usr/bin/mock", "--configdir", cfgdir, "shell", "--", "mkdir", "-p", "/var/cache/retrace-server/kernel"])
                retcode = call(["/usr/bin/mock", "--configdir", cfgdir, "--copyin", vmlinux, vmlinux], stdout=null, stderr=null)
                if retcode:
                    log += "Warning: copyin exited with %d; " % retcode

                child = Popen(["/usr/bin/mock", "--configdir", cfgdir, "shell", "--",
                               "crash", "-s", "/var/crash/vmcore", vmlinux], stdin=PIPE, stdout=PIPE, stderr=null)
                kernellog = child.communicate("log\nquit\n")[0]
                retcode = child.wait()
        finally:
            unlock(lockfile)
    else:
        if not os.path.isfile(vmlinux):
            wait = False
            try:
                os.mkdir(kerneltmp)
            except Exception as ex:
                if ex[0] == errno.EEXIST:
                    wait = True
                else:
                    log += "Error\nUnable to unpack required kernel debuginfo.\n"
                    fail(61)

            if wait:
                i = 0
                # another worker is unpacking the same debuginfo - wait for it to finish
                # 1800 seconds ~ 30 minutes
                while i < 1800 and not os.path.isfile(vmlinux):
                    time.sleep(1)
                    i += 1

                if i >= 1800:
                    log += "Error\nUnpacking required debuginfo timed out.\n"
                    fail(62)
            else:
                cache_vmlinux_from_debuginfo(kernelver, kernelver_noarch)

        task.set_status(STATUS_BACKTRACE)
        log += "OK\n%s " % STATUS[STATUS_BACKTRACE]

        child = Popen(["crash", "-s", vmcore, vmlinux], stdin=PIPE, stdout=PIPE, stderr=STDOUT)
        kernellog = child.communicate("log\nquit\n")[0]
        retcode = child.wait()

        if retcode:
            log += "Warning: crash exited with %d; " % retcode

    task.set_backtrace(kernellog)

    stats["duration"] = int(time.time()) - stats["starttime"]
    stats["status"] = STATUS_SUCCESS

    log += "OK\n%s " % STATUS[STATUS_STATS]

    try:
        save_crashstats(stats)
        log += "OK\n"
    except Exception as ex:
        log += "Error: %s\n" % ex

    # clean up temporary data
    task.set_status(STATUS_CLEANUP)
    log += "%s " % STATUS[STATUS_CLEANUP]

    if not task.get_type() in [TASK_VMCORE_INTERACTIVE]:
        task.clean()

    log += "OK\nRetrace took %d seconds.\n" % stats["duration"]
    log += "%s\n" % STATUS[STATUS_SUCCESS]

    task.set_log(log)
    task.set_status(STATUS_SUCCESS)

if __name__ == "__main__":
    if len(sys.argv) != 2:
        sys.stderr.write("Usage: %s task_id\n" % sys.argv[0])
        sys.exit(11)

    try:
        taskid = int(sys.argv[1])
    except:
        sys.stderr.write("Task ID may only contain digits.\n")
        sys.exit(12)

    try:
        task = RetraceTask(taskid)
    except:
        sys.stderr.write("Task '%s' does not exist.\n" % taskid)
        sys.exit(13)

    if task.has_status():
        sys.stderr.write("%s has already been executed " \
                         "for task '%d'.\n" % (sys.argv[0], taskid))
        sys.exit(10)

    # fork to background
    try:
        pid = os.fork()
    except:
        sys.stderr.write("Unable to fork")
        sys.exit(14)

    # parent - kill
    if pid != 0:
        sys.exit(0)

    # child - continue
    stats["taskid"] = taskid

    # get count of tasks running before starting
    prerunning = len(get_active_tasks()) - 1

    task.set_status(STATUS_ANALYZE)
    log += "%s " % STATUS[STATUS_ANALYZE]

    crashdir = os.path.join(task.get_savedir(), "crash")

    tasktype = task.get_type()

    # check the crash directory for required files
    for required_file in REQUIRED_FILES[tasktype]:
        if not os.path.isfile(os.path.join(crashdir, required_file)):
            log += "Error\nCrash directory does not contain required file '%s'.\n" % required_file
            fail(15)

    if tasktype in [TASK_RETRACE, TASK_DEBUG, TASK_RETRACE_INTERACTIVE]:
        start_retrace()
    elif tasktype in [TASK_VMCORE, TASK_VMCORE_INTERACTIVE]:
        start_vmcore()
    else:
        pass
