# backend.py - for the cli modules: handles processes and io
#
# (c) Copyright 2010 Michael Towers (larch42 at googlemail dot com)
#
# This file is part of the larch project.
#
#    larch 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.
#
#    larch 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 larch; if not, write to the Free Software Foundation, Inc.,
#    51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
#
#----------------------------------------------------------------------------
# 2010.11.09

# There was also the vague idea of a web interface, using a sort of state-
# based approach. Connecting to a running larch process would then require
# the ability to get the logging history, but presumably not the whole
# history on every ui update, which would need to be incremental.
# The logging function would need to be modified to accommodate this.

import os, sys, signal, atexit, __builtin__
import traceback, pwd
from subprocess import Popen, PIPE, STDOUT
import pexpect
try:
    import json as serialize
except:
    import simplejson as serialize
from config import *

def debug(text):
    sys.stderr.write("DEBUG: " + text.strip() + "\n")
    sys.stderr.flush()

sys.path.append(os.path.dirname(base_dir))
from liblarch.translation import i18n_module, lang
__builtin__._ = i18n_module(base_dir, 'larch')
__builtin__.lang = lang
# Run subprocesses without i18n in case the output is parsed.
os.environ["LANGUAGE"] = "C"


def init(app, options, app_quit=None):
    global _options, _quit_function, _log, _controlled, _dontask, _quiet
    _options = options
    _quit_function = app_quit
    _controlled = options.slave
    _dontask = options.force
    _log = None
    _quiet = False if _controlled else options.quiet

    atexit.register(sys_quit)
    if _controlled:
        _out('>-_$$_%d' % os.getpid())

    def sigint(num, frame):
        """A handler for SIGINT. Tidy up properly and quit.
        """
        errout("INTERRUPTED - killing subprocesses", 0)
        if _sub_process and _sub_process.pid:
            Popen(["pkill", "-g", str(_sub_process.pid)],
                    stdout=PIPE).communicate()
        errout("QUITTING", 2)
    signal.signal(signal.SIGINT, sigint)


    # Check no other instance of the script is running
    if os.path.isfile(LOCKFILE):
        app0 = readfile(LOCKFILE)
        if not query_yn(_(
                "larch (%s) seems to be running already."
                "\nIf you are absolutely sure this is not the case,"
                "\nyou may continue. Otherwise you should cancel."
                "\n\nShall I continue?") % app0):
            sys.exit(102)
    writefile(app, LOCKFILE)
    _log = open(LOGFILE + app, 'w')

    # For systems without /sbin and /usr/sbin in the normal PATH
    p = os.environ['PATH']
    ps = p.split(':')
    for px in ('/sbin', '/usr/sbin'):
        if px not in ps:
            p = px + ':' + p
    os.environ['PATH'] = p


def _out(text, force=False):
    """Send the string to standard output.
    How it is output depends on the '-s' command line option (whether the
    script is being run on the console or as a subprocess of another script).
    In the latter case the text will be slightly encoded - to avoid newline
    characters - and sent as a single unit.
    Otherwise output the lines as they are, but all lines except
    the first get a '--' prefix.
    """
    lines = text.encode('utf-8').splitlines()
    if _log and not text.startswith('>-'):
        # Don't log the progress report lines
        _log.write(lines[0] + '\n')
        for l in lines[1:]:
            _log.write('--' + l + '\n')

    if force or not _quiet:
        if _controlled:
            sys.stdout.write(serialize.dumps(text) + '\n')
        else:
            prefix = ''
            for line in lines:
                sys.stdout.write(prefix + line + '\n')
                prefix = '--'
        sys.stdout.flush()


def sys_quit():
    unmount()
    if _quit_function:
        _quit_function()
    if _errorcount:
        _out('!! ' + (_("The backend reported %d failed calls,"
                " you may want to investigate") % _errorcount))
    if _log:
        _log.close()
        os.remove(LOCKFILE)


def comment(text):
    _out('##' + text)


def query_yn(message):
    _out('?>' + message)
    if _dontask:
        result = True

    elif _controlled:
        result = (raw_input().strip() == '??YES')

    else:
        prompt = _("Yes:y|No:n")
        py, pn = prompt.split('|')
        respy = py.lower().split(':')
        respn = pn.lower().split(':')
        while True:
            resp = raw_input("   [ %s ]: " % prompt).strip().lower()
            if resp in respy:
                result = True
                break
            if resp in respn:
                result = False
                break

    _out('#>%s' % ('Yes' if result else 'No'))
    return result


def errout(message="ERROR", quit=1):
    _out('!>' + message, True)
    if quit:
        sys_quit()
        os._exit(quit)


def error0(message):
    errout(message, 0)
__builtin__.error0 = error0


# Catch all unhandled errors.
def errortrap(type, value, tb):
    etext = "".join(traceback.format_exception(type, value, tb))
    errout(_("Something went wrong:\n") + etext, 100)
sys.excepthook = errortrap


_sub_process = None
_errorcount = 0
def runcmd(cmd, filter=None):
    global _sub_process, _errorcount
    _out('>>' + cmd)
    _sub_process = pexpect.spawn(cmd)
    result = []
    line0 = ''
    # A normal end-of-line is '\r\n', so split on '\r' but don't
    # process a line until the next character is available.
    while True:
        try:
            line0 += _sub_process.read_nonblocking(size=256, timeout=None)
        except:
            break

        while True:
            lines = line0.split('\r', 1)
            if (len(lines) > 1) and lines[1]:
                line = lines[0]
                line0 = lines[1]
                nl = (line0[0] == '\n')
                if nl:
                    # Strip the '\n'
                    line0 = line0[1:]
                if filter:
                    nl, line = filter(line, nl)
                    if line == '/*/':
                        continue
                if nl:
                    line = line.rstrip()
                    _out('>_' + line)
                    result.append(line)
                else:
                    # Probably a progress line
                    if _controlled:
                        _out('>-' + line)
                    else:
                        sys.stdout.write(line + '\r')
                        sys.stdout.flush()

            else:
                break

    _sub_process.close()
    rc = _sub_process.exitstatus
    ok = (rc == 0)
    if not ok:
        _errorcount += 1
    _out(('>?%s' % repr(rc)) + ('' if ok else (' $$$ %s $$$' % cmd)))
    return (ok, result)


def script(cmd):
    s = runcmd("%s/%s" % (script_dir, cmd))
    if s[0]:
        return ""
    else:
        return "SCRIPT ERROR: (%s)\n" % cmd + "".join(s[1])


def chroot(ip, cmd, mnts=[], filter=None):
    if ip:
        for m in mnts:
            mdir = "%s/%s" % (ip, m)
            if not os.path.isdir(mdir):
                runcmd('mkdir -p %s' % mdir)
            mount("/" + m, mdir, "--bind")
        cmd = "chroot %s %s" % (ip, cmd)

    s = runcmd(cmd, filter)

    if ip:
        unmount(["%s/%s" % (ip, m) for m in mnts])

    if s[0]:
        if s[1]:
            return s[1]
        else:
            return True
    return False


_mounts = []
def mount(src, dst, opts=""):
    if runcmd("mount %s %s %s" % (opts, src, dst))[0]:
        _mounts.append(dst)
        return True
    return False


def unmount(dst=None):
    if dst == None:
        mnts = list(_mounts)
    elif type(dst) in (list, tuple):
        mnts = list(dst)
    else:
        mnts = [dst]

    r = True
    for m in mnts:
        if runcmd("umount %s" % m)[0]:
            _mounts.remove(m)
        else:
            r = False
    return r


def get_installation_dir():
    return os.path.realpath(_options.idir if _options.idir
            else INSTALLATION)


def get_profile():
    """Get the absolute path to the profile folder given its path in any
    acceptable form, including 'user:profile-name'
    """
    pd = (_options.profile if _options.profile
            else base_dir + '/profiles/default')
    p = pd.split(':')
    if len(p) == 1:
        pd = os.path.realpath(pd)
    else:
        try:
            pd = (pwd.getpwnam(p[0])[5] + PROFILE_DIR
                    + '/' + p[1])
        except:
            errout(_("Invalid profile: %s") % pd, quit=0)
            raise
    if not os.path.isfile(pd + '/addedpacks'):
        errout(_("Invalid profile folder: %s") % pd)
    return pd



#+++++++++++++++++++++++++++++++++++++++++
#Regular expression search strings for progress reports
import re
#lit: give []() a \-prefix
#grp: surround string in ()
#opt: surround string in []

def _lit(s):
    for c in r'[()]':
        s = s.replace(c, '\\' + c)
    return s

def _grp(s, x=''):
    return '(' + s + ')' + x

def _grp0(s, x=''):
    return '(?:' + s + ')' + x

def _opt(s, x=''):
    return '[' + s + ']' + x

re_psub = re.compile(r'\[[#-]+\]')
_re_pacman = re.compile( _grp0(_lit('(') +
                            _grp(_opt('^/', '+') + '/' + _opt('^)', '+')) +
                            _lit(')'), '?') +
                        _grp('.*?') +
                        _lit('[') + _grp(_opt('-#', '+')) + _lit(r']\s+') +
                        _grp(_opt('0-9', '+')) +
                        '%'
                        )

_re_mksquashfs = re.compile(_lit('[.*]') +
                            _grp('.* ' +
                                _grp(_opt('0-9', '+')) +
                                '%')
                            )

_re_mkisofs = re.compile(_opt(' 1') + _opt(' \d') + '\d\.\d\d%')

#-----------------------------------------
class pacman_filter_gen:
    """Return a function to detect and process the progress output of
    pacman.
    """
    def __init__(self):
        self.progress = ''

    def __call__(self, line, nl):
        ms = _re_pacman.match(line)
        if ms:
            p = ms.group(3)
            if (self.progress != p) or nl:
                self.progress = p
                xfromy = ms.group(1)
                if _controlled:
                    if not xfromy:
                        xfromy = ''
                    line = 'pacman:%s|%s|%s%%' % (xfromy, ms.group(2),
                            ms.group(4))
                elif ms.group(4) == '100':
                    line = re_psub.sub('[##########]', line)
                if nl:
                    sys.stdout.write(' '*80 + '\r')
            else:
                line = '/*/'
        return (nl, line)


class mksquashfs_filter_gen:
    """Return a function to detect and process the progress output of
    mksquashfs.
    """
    def __init__(self):
        self.progress = ''

    def __call__(self, line, nl):
        ms = _re_mksquashfs.match(line)
        if ms:
            percent = ms.group(2)
            if (self.progress != percent) or nl:
                self.progress = percent
                if _controlled:
                    line = 'mksquashfs:' + ms.group(1)
                else:
                    line = re.sub(r'=[-\\/|]', '= ', line)
            else:
                line = '/*/'
        return (nl, line)


class mkisofs_filter_gen:
    """Return a function to detect and process the progress output of
    mkisofs.
    """
    def __init__(self):
        self.running = None

    def __call__(self, line, nl):
        ms = _re_mkisofs.match(line)
        if ms:
            if _controlled:
                line = 'mkisofs:' + line
            self.running = line
            nl = False
        elif self.running:
            line = self.running + '\n' + line
            self.running = None
        return (nl, line)


def readdata(filename):
    return readfile(base_dir + '/data/' + filename)


def readfile(fpath):
    try:
        fh = open(fpath)
        text = fh.read()
        fh.close()
    except:
        errout(_("Couldn't read file: %s") % fpath)
        return None
    return text


def writefile(text, path):
    try:
        pd = os.path.dirname(path)
        if not os.path.isdir(pd):
            os.makedirs(pd)
        fh = None
        fh = open(path, 'w')
        fh.write(text)
        return True
    except:
        return False
    finally:
        if fh:
            fh.close()