#!/usr/bin/python

# Copyright (C) 2007 - 2011 Canonical Ltd.
# Author: Martin Pitt <martin.pitt@ubuntu.com>
# 
# 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.  See http://www.gnu.org/copyleft/gpl.html for
# the full text of the license.

import os, time, optparse, subprocess, sys, signal, zlib, errno, stat, shutil

import apport
from apport.crashdb import get_crashdb

#
# classes
#

class CrashDigger:
    def __init__(self, config_dir, auth_file, cache_dir, apport_retrace,
            verbose=False, dup_db=None, dupcheck_mode=False):
        '''Initialize pools.'''

        self.retrace_pool = set()
        self.dupcheck_pool = set()
        self.config_dir = config_dir
        self.cache_dir = cache_dir
        self.verbose = verbose
        self.auth_file = auth_file
        self.dup_db = dup_db
        self.dupcheck_mode = dupcheck_mode
        self.crashdb = get_crashdb(auth_file)
        self.apport_retrace = apport_retrace
        if config_dir:
            self.releases = os.listdir(config_dir)
            self.releases.sort()
            self.log('Available releases: %s' % str(self.releases))
        else:
            self.releases = None

        if self.dup_db:
            self.crashdb.init_duplicate_db(self.dup_db)
            # this verified DB integrity; make a backup now
            shutil.copy2(self.dup_db, self.dup_db + '.backup')

    def log(self, str):
        '''If verbosity is enabled, log the given string to stdout, and prepend
        the current date and time.'''

        sys.stdout.write('%s: %s\n' % (time.strftime('%x %X'), str))
        sys.stdout.flush()

    def fill_pool(self):
        '''Query crash db for new IDs to process.'''

        if self.dupcheck_mode:
            self.dupcheck_pool.update(self.crashdb.get_dup_unchecked())
            self.log('fill_pool: dup check pool now: %s' % str(self.dupcheck_pool))
        else:
            self.retrace_pool.update(self.crashdb.get_unretraced())
            self.log('fill_pool: retrace pool now: %s' % str(self.retrace_pool))

    def retrace_next(self):
        '''Grab an ID from the retrace pool and retrace it.'''

        id = self.retrace_pool.pop()
        self.log('retracing #%i (left in pool: %i)' % (id, len(self.retrace_pool)))

        try:
            rel = self.crashdb.get_distro_release(id)
        except ValueError:
            self.log('could not determine release -- no DistroRelease field?')
            self.crashdb.mark_retraced(id)
            return
        if rel not in self.releases:
            self.log('crash is release %s which does not have a config available, skipping' % rel)
            return

        argv = [self.apport_retrace, '-S', self.config_dir, '--auth', self.auth_file]
        if self.cache_dir:
            argv += ['--cache', self.cache_dir]
        if self.dup_db:
            argv += ['--duplicate-db', self.dup_db]
        if self.verbose:
            argv.append('-v')
        argv.append(str(id))

        result = subprocess.call(argv, stdout=sys.stdout,
            stderr=subprocess.STDOUT)
        if result != 0:
            self.log('retracing #%i failed with status: %i' % (id, result))
            if result == 99:
                self.retrace_pool = set()
                self.log('transient error reported; halting')
                return
            raise SystemError('retracing #%i failed' % id)

        self.crashdb.mark_retraced(id)

    def dupcheck_next(self):
        '''Grab an ID from the dupcheck pool and process it.'''

        id = self.dupcheck_pool.pop()
        self.log('checking #%i for duplicate (left in pool: %i)' % (id, len(self.dupcheck_pool)))

        try:
            report = self.crashdb.download(id)
        except (MemoryError, TypeError, ValueError, IOError, zlib.error) as e:
            self.log('Cannot download report: ' + str(e))
            apport.error('Cannot download report %i: %s', id, str(e))
            return

        res = self.crashdb.check_duplicate(id, report)
        if res:
            if res[1] == None:
                self.log('Report is a duplicate of #%i (not fixed yet)' % res[0])
            elif res[1] == '':
                self.log('Report is a duplicate of #%i (fixed in latest version)' % res[0])
            else:
                self.log('Report is a duplicate of #%i (fixed in version %s)' % res)
        else:
            self.log('Duplicate check negative')

    def run(self):
        '''Process the work pools until they are empty.'''

        self.fill_pool()
        while self.dupcheck_pool:
            self.dupcheck_next()
        while self.retrace_pool:
            self.retrace_next()

#
# functions
#

def parse_options():
    '''Parse command line options and return (options, args) tuple.'''

    optparser = optparse.OptionParser('%prog [options]')
    optparser.add_option('-c', '--config-dir', metavar='DIR',
        help='Packaging system configuration base directory.')
    optparser.add_option('-C', '--cache', metavar='DIR',
        help='Cache directory for packages downloaded in the sandbox')
    optparser.add_option('-a', '--auth',
        help='Path to a file with the crash database authentication information.',
        action='store', type='string', dest='auth_file', default=None)
    optparser.add_option('-l', '--lock',
        help='Lock file; will be created and removed on successful exit, and '
            'program immediately aborts if it already exists',
        action='store', dest='lockfile', default=None)
    optparser.add_option('-d', '--duplicate-db',
        help='Path to the duplicate sqlite database (default: disabled)',
        action='store', type='string', dest='dup_db', metavar='PATH',
        default=None)
    optparser.add_option('-D', '--dupcheck',
        help='Only check duplicates for architecture independent crashes (like Python exceptions)',
        action='store_true', dest='dupcheck_mode', default=False)
    optparser.add_option('-v', '--verbose',
        help='Verbose operation (also passed to apport-retrace)',
        action='store_true', dest='verbose', default=False)
    optparser.add_option('--apport-retrace', metavar='PATH',
        help='Path to apport-retrace script (default: directory of crash-digger or $PATH)')

    (opts, args) = optparser.parse_args()

    if not opts.config_dir and not opts.dupcheck_mode:
        apport.fatal('Error: --config-dir or --dupcheck needs to be given')
    if not opts.auth_file:
        apport.fatal('Error: -a/--auth needs to be given')
    
    return (opts, args)

#
# main
#

opts, args = parse_options()


# support running from tree, then fall back to $PATH
if not opts.apport_retrace:
    opts.apport_retrace = os.path.join(os.path.dirname(sys.argv[0]),
            'apport-retrace')
    if not os.access(opts.apport_retrace, os.X_OK):
        opts.apport_retrace = 'apport-retrace'

if opts.lockfile:
    try:
        f = os.open(opts.lockfile, os.O_WRONLY|os.O_CREAT|os.O_EXCL, 0o666)
        os.close(f)
    except OSError as e:
        if e.errno == errno.EEXIST:
            sys.exit(0)
        else:
            raise

try:
    CrashDigger(opts.config_dir, opts.auth_file, opts.cache,
            opts.apport_retrace, opts.verbose, opts.dup_db,
        opts.dupcheck_mode).run()
except SystemExit as exit:
    if exit.code == 99:
        pass # fall through lock cleanup
    else:
       raise

if opts.lockfile:
    os.unlink(opts.lockfile)
