#!/usr/bin/env python
# -*- mode: python; coding: utf-8 -*-
# vim:smartindent cinwords=if,elif,else,for,while,try,except,finally,def,class:ts=4:sts=4:sta:et:ai:shiftwidth=4
#
# arch-tag: Simple patch queue manager for tla
# Copyright © 2003,2004 Colin Walters <walters@verbum.org>
# Copyright ©  2004 Canonical Ltd. 
#	Author: Robert Collins <robertc@robertcollins.net>
# Copyright © 2003, 2005 Walter Landry

# 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 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

# Some junk to try finding Python 2.3, if "python" on this system
# is too old.
import os,sys
if sys.hexversion >= 0x2030000:
    pass
else:
    if os.getenv('PYTHON'):
        try:
            os.execvp(os.getenv('PYTHON'), [os.getenv('PYTHON')] + sys.argv)
        except:
            1
    try:
        os.execvp('python2.3', ['python2.3'] + sys.argv)
    except:
        1
    sys.stderr.write("This program requires Python 2.3\n")
    sys.exit(1)

import string, stat, re, glob, getopt, time, traceback, gzip, getpass, popen2
import smtplib, email
import logging, logging.handlers
import pqm
from pqm import *


def usage(ecode, ver_only=None):
    print "pqm 0"
    if ver_only:
        sys.exit(ecode)
    print "Usage: pqm [OPTIONS...] [DIRECTORY]"
    print "Options:"
    print "  -v, --verbose\t\tDisplay extra information"
    print "  -q, --quiet\t\tDisplay less information"
    print "  -c, --config=FILE\tParse configuration info from FILE"
    print "  -d, --debug\t\tOutput information to stdout as well as log"
    print "  --no-log\t\tDon't write information to log file"
    print "  -n, --no-act\t\tDon't actually perform changes"
    print "  -r, --read\t\tRead a request from stdin"
    print "  --run\t\tProcess queue"
    print "  --report\t\tPrint patch report (used with --run)"
    print "  --no-verify\t\tDon't verify signatures"
    print "  --queuedir=DIR\t\tPerform first-time configuration"
    print "  --keyring=FILE\t\tUse the specified GPG keyring"
    print "  --help\t\tWhat you're looking at"
    print "  --version\t\tPrint the software version and exit"
    sys.exit(ecode)

def do_mkdir(name):
    if os.access(name, os.X_OK):
        return    
    try:
        logger.info('Creating directory "%s"' % (name))
    except:
        pass
    if not no_act:
        os.mkdir(name)

def do_rename(source, target):
    try:
        logger.debug('Renaming "%s" to "%s"' % (source, target))
    except:
        pass
    if not no_act:
        os.rename(source, target)

def do_chmod(name, mode):
    try:
        logger.info('Changing mode of "%s" to %o' % (name, mode))
    except:
        pass
    if not no_act:
        os.chmod(name, mode)

def dir_from_option(configp, option, default):
    """calculate a working dir path"""
    return os.path.abspath(os.path.expanduser(configp.get_option('DEFAULT',option, os.path.join(queuedir, default))))

class RevisionOptionHandler:
    def __init__(self, revisions, configp):
        self._configp = configp
        self._revisions = revisions
        self._optionmap = {}
        self._optionmap['precommit_hook'] = ['str', None]
        self._optionmap['published_at'] = ['str', None]
        self._optionmap['build_config'] = ['str', None]
        self._optionmap['build_dir'] = ['str', None]
        self._optionmap['commiters'] = ['str', None]
        self._optionmap['commit_re'] = ['str', None]

    def get_option_map(self, dist):
        ret = self._revisions[dist]
        for key in self._optionmap.keys():
            type = self._optionmap[key][0]
            ret[key] = self._optionmap[key][1]
            if self._configp.has_option ('DEFAULT', key):
                ret[key] = self.get_option (type, 'DEFAULT', key)
            if self._configp.has_option (dist, key):
                ret[key] = self.get_option (type, dist, key)
        return ret            

    def get_option (self, type, dist, key):
        if type == 'int':
            return self._configp.getint(dist, key)
        elif type == 'str':
            return self._configp.get(dist, key)
        elif type == 'bool':
            return self._configp.getboolean(dist, key)

        assert(None)

def runtla_internal(sender, cmd, *args):
    return apply(popen_noshell, [arch_path, cmd] + list(args))

def runtla(sender, cmd, *args):
    (status, msg, output) = apply(runtla_internal, [sender, cmd] + list(args))
    if not ((status is None) or (status == 0)):
        raise PQMTlaFailure(sender, ["VCS command %s %s failed (%s): %s" % (cmd, args, status, msg)] + output)
    return output

class LockFile(object):
    """I represent a lock that is made on the file system, to prevent concurrent execution of this code"""
    def __init__(self, filename):
        self.filename=filename
        self.locked=False
    def acquire(self):
        if no_act:
            return
        logger.info('creating lockfile')
        try:
            os.open(self.filename, os.O_CREAT | os.O_EXCL)
            self.locked=True
        except OSError, e:
            if cron_mode:
                logger.info("lockfile %s already exists, exiting", self.filename)
                sys.exit(0)
            else:
                logger.error("Couldn't create lockfile: %s", self.filename)
                sys.exit(1)
    def release(self):
        if not self.locked:
            return
        if no_act:
            return
        logger.debug('Removing lock file: %s', self.filename)
        os.unlink(self.filename)
        self.locked=False

def do_run_mode(queuedir, logger, logdir, mail_reply, mail_server, from_address, fromaddr, print_report):
    scripts = find_patches(queuedir, logger, verify_sigs)
    (goodscripts, badscripts) = ([], [])
    for script in scripts:
        run_one_script(logger, script, logdir, goodscripts, badscripts, mail_reply, mail_server, from_address, fromaddr)
            
    if print_report:
        for (patchname, logname) in goodscripts:
            print "Patch: " + patchname
            print "Status: success"
            print "Log: " + logname
            print
        for (patchname, logname) in badscripts:
            print "Patch: " + patchname
            print "Status: failure"
            print "Log: " + logname
            print

def run_one_script(logger, script, logdir, goodscripts, badscripts, mail_reply, mail_server, from_address, fromaddr):
    try:
        logger.info('trying script ' + script.filename)
        logname = os.path.join(logdir, os.path.basename(script.filename) + '.log')
        (sender, subject, msg, sig) = read_email(logger, open(script.filename))
        if verify_sigs:
            sigid,siguid = verify_sig(script.getSender(), msg, sig, 0, logger)
        success = False
        output = []
        failedcmd=None

        # ugly transitional code
        pqm.allowed_revisions = allowed_revisions
        pqm.logger = logger
        pqm.workdir = workdir
        pqm.runtla = runtla
        pqm.precommit_hook = precommit_hook
        cmd=CommandRunner()
        cmd.arch_impl = arch_impl
        cmd.arch_path = arch_path
        (successes, unrecognized, output) = cmd.run(script, siguid)
        logger.info('successes: %s' % (successes,))
        logger.info('unrecognized: %s' % (unrecognized,))
        success = True
        goodscripts.append((script.filename, logname))
    except pqm.PQMCmdFailure, e:
        badscripts.append((script.filename, logname))
        successes = e.goodcmds
        failedcmd = e.badcmd
        output = e.output
        unrecognized=[]
    except PQMException, e:
        badscripts.append((script.filename, logname))
        successes = []
        failedcmd = []
        output = [str(e)]
        unrecognized=[]
    log_list(logname, output)
    os.unlink(script.filename)
    if mail_reply:
        send_mail_reply(success, successes, unrecognized,
                        mail_server, from_address, script.getSender(),
                        fromaddr, failedcmd, output, cmd)
    else:
        logger.info('not sending mail reply')

def gather_output(retmesg, output):
    result=''
    for line in output:
        result += '\n%s' % line
    return result

def send_mail_reply(success, successes, unrecognized, mail_server, from_address, sender, fromaddr, failedcmd, output, cmd):
    if success:
        retmesg = mail_format_successes(successes, "Command was successful.", unrecognized, line)
        if len(successes) > 0:
            statusmsg='success'
        else:
            statusmsg='no valid commands given'
    else:
        retmesg = mail_format_successes(successes, "Command passed checks, but was not committed.", unrecognized, line)
        retmesg+= "\n%s" % failedcmd
        retmesg+= '\nCommand failed!'
        if not cmd.debug:
            retmesg+= '\nLast 20 lines of log output:'
            retmesg += gather_output (retmesg, output[-20:])
        else:
            retmesg+= '\nAll lines of log output:'
            retmesg += gather_output (retmesg, output)
        statusmsg='failure'
    server = smtplib.SMTP(mail_server)
    server.sendmail(from_address, [sender], 'From: %s\r\nTo: %s\r\nSubject: %s\r\n\r\n%s\n' % (fromaddr, sender, statusmsg, retmesg))
    server.quit()

def mail_format_successes(successes, command_msg, unrecognized, line):
    retmesg = []
    for success in successes:
        retmesg.append('> ' + success)
        retmesg.append(command_msg)
        for line in unrecognized:
            retmesg.append('> ' + line)
            retmesg.append('Unrecognized command.')
    return string.join(retmesg, '\n')

def log_list(logname, list):
    f = open(logname, 'w')
    for l in list:
        f.write(l)
    f.close()

def run(pqm_subdir, run_mode, queuedir, logger, logdir, mail_reply, mail_server, from_address, fromaddr, print_report):
    lockfile=LockFile(os.path.join(pqm_subdir, 'pqm.lock'))
    lockfile.acquire()
    try:
        if run_mode:
            do_run_mode(queuedir, logger, logdir, mail_reply, mail_server, from_address, fromaddr, print_report)
    finally:
        lockfile.release()
        
def do_read_mode(logger):
    sender = None
    try:
        (sender, subject, msg, sig) = read_email(logger)
        if verify_sigs:
            sigid,siguid = verify_sig(sender, msg, sig, 1, logger)
            open(transaction_file, 'a').write(sigid + '\n')
        fname = 'patch.%d' % (time.time())
        logger.info('new patch ' + fname)
        f = open('tmp.' + fname, 'w')
        f.write('From: ' + sender + '\n')
        f.write('Subject: ' + subject + '\n')
        f.write(string.join(re.split('\r?\n', msg), '\n')) # canonicalize line endings
        f.close()
        os.rename('tmp.' + fname, fname)
    except:
        if sender and mail_reply:
            server = smtplib.SMTP(mail_server)
            tb=string.join(traceback.format_exception(sys.exc_type, sys.exc_value, sys.exc_traceback), '')
            server.sendmail(from_address, [sender], 'From: %s\r\nTo: %s\r\nSubject: error processing requests\r\n\r\n' % (fromaddr, sender) + 'An error was encountered:\n' + tb)
            server.quit()
        logger.exception("Caught exception")
        sys.exit(1)
    sys.exit(0)

arch_path = 'arx'
#arch_path = 'baz'
arch_impl = None
logfile_name = 'pqm.log'
default_mail_log_level = logging.ERROR
mail_server = 'localhost'
queuedir = None
workdir = None
logdir = None
mail_reply = 1
verify_sigs = 1
from_address = None
allowed_revisions = {}
precommit_hook = []

try:
    opts, args = getopt.getopt(sys.argv[1:], 'vqc:dnrk',
                               ['verbose', 'quiet', 'config=', 'debug', 'no-log',
                                'no-act', 'read', 'run', 'report', 'cron', 'no-verify',
                                'queuedir=', 'keyring=', 'help', 'version', ])
except getopt.GetoptError, e:
    sys.stderr.write("Error reading arguments: %s\n" % e)
    usage(1)
for (key, val) in opts:
    if key == '--help':
        usage(0)
    elif key == '--version':
        usage(0, ver_only=1)
if len(args) > 1:
    sys.stderr.write("Unknown arguments: %s\n" % args[1:])
    usage(1)

logger = logging.getLogger("pqm")

loglevel = logging.WARN
no_act = 0
debug_mode = 0
run_mode = 0
read_mode = 0
cron_mode = 0
print_report = 0
no_log = 0
batch_mode = 0
custom_config_files = 0
for key, val in opts:
    if key in ('-v', '--verbose'):
        if loglevel == logging.INFO:
            loglevel = logging.DEBUG
        elif loglevel == logging.WARN:
            loglevel = logging.INFO
    elif key in ('-q', '--quiet'):
        if loglevel == logging.WARN:
            loglevel = logging.ERROR
        elif loglevel == logging.WARN:
            loglevel = logging.CRITICAL
    elif key in ('-c', '--config'):
        if not custom_config_files:
            custom_config_files = 1
            configfile_names = []
        configfile_names.append(os.path.abspath(os.path.expanduser(val)))
    elif key in ('--keyring'):
        pqm.keyring = val
    elif key in ('-n', '--no-act'):
        no_act = 1
    elif key in ('-d', '--debug'):
        debug_mode = 1
    elif key in ('--queuedir',):
        queuedir = val
    elif key in ('--keyring',):
        pqm.keyring = val
    elif key in ('--no-log',):
        no_log = 1
    elif key in ('--no-verify',):
        verify_sigs = 0
    elif key in ('-r', '--read'):
        read_mode = 1
    elif key in ('--run',):
        run_mode = 1
    elif key in ('--cron',):
        cron_mode = 1
    elif key in ('--report',):
        print_report = 1


logger.setLevel(logging.DEBUG)
stderr_handler = logging.StreamHandler(strm=sys.stderr)
stderr_handler.setLevel(loglevel)
logger.addHandler(stderr_handler)
stderr_handler.setLevel(loglevel)
stderr_handler.setFormatter(logging.Formatter(fmt="%(name)s [%(thread)d] %(levelname)s: %(message)s"))

if not (read_mode or run_mode):
    logger.error("Either --read or --run must be specified")
    sys.exit(1)

configp = ConfigParser()
configfile_names = map(lambda x: os.path.abspath(os.path.expanduser(x)), configfile_names)
logger.debug("Reading config files: %s" % (configfile_names,))
configp.read(configfile_names)

if configp.has_option('DEFAULT', 'arch_path'):
    arch_path = configp.get('DEFAULT', 'arch_path')
elif configp.has_option('DEFAULT', 'tlapath'): 
    logger.warn("Option 'tlapath' is deprecated")
    arch_path = configp.get('DEFAULT', 'tlapath')

if configp.has_option('DEFAULT', 'groups'):
    for group in configp.get('DEFAULT', 'groups').split(','):
        groups[group.strip()]=[]
    logger.info('found groups %s', groups)
    for group in groups.keys():
        for member in configp.get(group, 'members').split(','):
            groups[group].append(member.strip())
        logger.info('group %s has members %s', group, groups[group])

if os.access(arch_path, os.X_OK):
    logger.error("Can't execute \"%s\", please fix arch_path" % (arch_path,))
    sys.exit(1)

# ugly transitional code
pqm.logger = logger
if configp.has_option('DEFAULT', 'arch_impl'):
    impl = configp.get('DEFAULT', 'arch_impl')
    if impl == 'tla':
        arch_impl = TlaHandler()
    elif impl == 'arx':
        arch_impl = ArXHandler()
    elif impl == 'baz':
        arch_impl = Baz1_1Handler()
    elif impl == 'baz1.0':
        arch_impl = Baz1_0Handler()
    else:
        logger.error("Unknown arch_impl \"%s\"" % (impl,))
        sys.exit(1)

pqm.gpgv_path = configp.get_option('DEFAULT', 'gpgv_path', 'gpgv')
myname = configp.get_option('DEFAULT', 'myname', 'Arch Patch Queue Manager')

if configp.has_option('DEFAULT', 'from_address'):
    from_address = configp.get('DEFAULT', 'from_address')
else:
    logger.error("No from_address specified")
    sys.exit(1)
fromaddr = '%s <%s>' % (myname, from_address)

mail_reply=configp.get_boolean_option('DEFAULT', 'mail_reply',1)
verify_sigs=configp.get_boolean_option('DEFAULT', 'verify_sigs', verify_sigs)

if not queuedir:
    queuedir = get_queuedir (configp, logger, args)
queuedir=os.path.abspath(queuedir)

if not configp.has_option('DEFAULT', 'dont_set_home'):
	os.environ['HOME'] = queuedir

workdir=dir_from_option(configp, 'workdir', 'workdir')
logdir=dir_from_option(configp, 'logdir', 'logs')

if not pqm.keyring:
    if configp.has_option('DEFAULT', 'keyring'):
        pqm.keyring = configp.get('DEFAULT', 'keyring')
    else:
        logger.error("No keyring specified on command line or in config files.")
        sys.exit(1)
if not os.access(pqm.keyring, os.R_OK):
    logger.error("Couldn't access keyring %s" % (pqm.keyring,))
    sys.exit(1)

sects = configp.sections()
if len(sects) > 0:
    for sect in sects:
        if str(sect) in groups.keys():
            continue
        logger.info("managing revision: " + sect)
        allowed_revisions[sect] = {}
else:
    logger.error("No revisions to manage!")
    sys.exit(1)
    

rev_optionhandler = RevisionOptionHandler(allowed_revisions, configp)

for rev in allowed_revisions.keys():
    allowed_revisions[rev] = rev_optionhandler.get_option_map(rev)

do_mkdir(queuedir)
os.chdir(queuedir)
do_mkdir(workdir)
do_mkdir(logdir)
pqm_subdir = os.path.join(queuedir, 'arch-pqm')
pqm.pqm_subdir = pqm_subdir
do_mkdir(pqm_subdir)

if configp.has_option('DEFAULT', 'logfile'):
    logfile_name = configp.get('DEFAULT', 'logfile')

if not no_log:
    if not os.path.isabs(logfile_name):
        logfile_name = os.path.join(pqm_subdir, logfile_name)
    logger.debug("Adding log file: %s" % (logfile_name,))
    filehandler = logging.FileHandler(logfile_name)
    if loglevel == logging.WARN:
        filehandler.setLevel(logging.INFO)
    else:
        filehandler.setLevel(logging.DEBUG)
    logger.addHandler(filehandler)
    filehandler.setFormatter(logging.Formatter(fmt="%(asctime)s %(name)s [%(thread)d] %(levelname)s: %(message)s", datefmt="%b %d %H:%M:%S"))

if not (debug_mode or batch_mode):
    # Don't log to stderr past this point
    logger.removeHandler(stderr_handler)

transaction_file = os.path.join(queuedir, 'transactions-completed')
if os.access(transaction_file, os.R_OK):
    lines = open(transaction_file).readlines()
    for line in lines:
        pqm.used_transactions[line[0:-1]] = 1

if read_mode:
    do_read_mode(logger)

assert(run_mode)

run(pqm_subdir, run_mode, queuedir, logger, logdir, mail_reply, mail_server, from_address, fromaddr, print_report)
logger.info("main thread exiting...")
sys.exit(0)

