#!/usr/bin/env python2
#
# rootrun.py   --  Running commands as root, using pexpect with su or sudo
#
# (c) Copyright 2010 Michael Towers (larch42 at googlemail dot 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.
#
# 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., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
#
#-------------------------------------------------------------------
# 2010.08.08


import os, threading, pexpect
from subprocess import Popen, PIPE, STDOUT
from collections import deque

from liblarch_conf import sudo, sudoprompt

from translation import i18n_liblarch
_ = i18n_liblarch()


class FnChain:
    """Implements a simple dynamic function chain.
    The supplied function call is executed in a new thread. Further
    (pending) function calls can be added to the fifo list, self.fnlist,
    which is a queue for execution when the current function returns.
    When the list is empty, the chain is complete.
    """
    def __init__(self, fn, *args):
        self.fnlist = deque()
        self.step(fn, *args)
        self.pending = {}
        self.flagcount = 0
        threading.Thread(target=self._fnloop, args=()).start()

    def _fnloop(self):
        while True:
            try:
                fn, args = self.fnlist.popleft()
            except IndexError:
                return
            fn(*args)

    def step(self, fn, *args):
        self.fnlist.append((fn, args))

    def callback(self, cb, continuation, *args):
        if continuation:
            self.flagcount += 1
            myflag = self.flagcount
            if cb:
                self.pending[myflag] = continuation
        else:
            myflag = None
        if cb:
            cb(myflag, *args)
            # When the callback completes it needs to call the
            # callback_done method, passing the flag and the results,
            # unless the flag is None.
        elif continuation:
            self.step(continuation, *args)

    def callback_done(self, flag, *args):
        self.step(self.pending[flag], *args)
        del(self.pending[flag])



class _RootCommand:
    """This class allows shell commands to be run as root by non-root users.
    This is achieved using pexpect to negotiate with either su or sudo
    (selectable in the configuration file).
    The run method allows interaction with the running process on a
    line-by-line basis, if desired, using callbacks.
    The call method is a non-interactive wrapper to allow simple running
    of shell scripts as root, returning completion code and output text.
    """
    def __init__(self, callback_pw):
        """Initialize the instance
        """
        self.password = None
        self.callback_pw = callback_pw
        self.fnchain = None


    def cb_done(self, flag, *args):
        """Called by a callback to pass its result back.
        """
        if flag:
            self.fnchain.callback_done(flag, *args)


    def run(self, cmd, end_callback, line_callback=None, cwd=None):
        self.cmd = cmd
        self.line_callback = line_callback
        self.end_callback = end_callback
        self.cwd = cwd
        if self.fnchain:
            debug("Previous FnChain instance not terminated")
        self.fnchain = FnChain(self._run_start)

    def _run_start(self, message=None):
        if sudo:
            self.process = pexpect.spawn('sudo -p "%s" bash -c "echo ___ && %s"'
                    % (sudoprompt, self.cmd), cwd=self.cwd, timeout=None)
            self.password_prompt = sudoprompt
            if not message:
                message = _("Please enter (sudo) password:")

        else:       # use su
            self.process = pexpect.spawn('su -c "echo ___ && %s"' % self.cmd,
                    cwd=self.cwd, timeout=None)
            self.password_prompt = ':'
            if not message:
                message = _("Please enter root password:")

        if self.process.expect([self.password_prompt, '\r\n']) == 0:
            if self.password:
                self.fnchain.step(self._run_sendpw)
            else:
                self.fnchain.callback(self.callback_pw, self._run_pw, message)

        else:
            self.fnchain.step(self._run_readloop)

    def _run_pw(self, ok, pw):
        if ok:
            self.password = pw
            self.fnchain.step(self._run_sendpw)
        else:
            self.process.close(force=True)
            # Avoid calling the end callback before self.fnchain is reset (threads!)
            _fnchain = self.fnchain
            self.fnchain = None
            _fnchain.callback(self.end_callback, None, False, _("Operation cancelled"))

    def _run_sendpw(self):
        self.process.sendline(self.password)
        while True:
            line1 = self.process.readline().strip()
            if line1:
                break
        if line1 == '___':
            self.fnchain.step(self._run_readloop)
        else:
            self.process.close(force=True)
            self.password = None
            self.fnchain.step(self._run_start, _("Incorrect password, try again:"))

    def _run_readloop(self):
        """The read loop collects output from the subprocess line by line
        and passes it on via the line callback. When the process has finished
        the end callback is passed the complete output text.
        """
        lines = ""
        while True:
            line = self.process.readline()
            if not line:                # Process finished
                break
            line = line.rstrip()
            lines += line + '\n'
            self.fnchain.callback(self.line_callback, None, line)

        # From the pexpect docs:
        # If you wish to get the exit status of the child you must call the
        # close() method. The exit or signal status of the child will be stored
        # in self.exitstatus or self.signalstatus. If the child exited normally
        # then exitstatus will store the exit return code and signalstatus will
        # be None. If the child was terminated abnormally with a signal then
        # signalstatus will store the signal value and exitstatus will be None.
        self.process.close()
        # Avoid calling the end callback before self.fnchain is reset (threads!)
        _fnchain = self.fnchain
        self.fnchain = None
        _fnchain.callback(self.end_callback, None, self.process.exitstatus == 0, lines)


    def interrupt(self):
        # Need to start a second pexpect to interrupt as root
        if sudo:
            pexpect.run('sudo -p "%s" kill -2 %d"' % (self.password_prompt, self.pid),
                    events={self.password_prompt: self.password+'\n'})
        else:
            pexpect.run('su -c "kill -2 %d"' % self.pid,
                    events={':': self.password+'\n'})

#    def kill(self):
#        self.process.close(force=True)

    def setpid(self, pid):
        """For kill to work as desired the pid to which the signal should
        be sent must first be set.
        """
        self.pid = pid


    def send(self, cmd):
        self.process.sendline(cmd)


    def call(self, cmd, cwd=None):
        self.event = threading.Event()
        self.run(cmd, self._end_callback0)
        self.event.wait()
        return self.retv


    def _end_callback0(self, ok, text):
        self.retv = (ok, text)
        self.event.set()



_rc = None
def init_rootrun(callback_pw):
    global _rc
    if not _rc:
        _rc = _RootCommand(callback_pw)
    return _rc


def runasroot(cmd, cwd=None, pwget=None):
    """Wrap RootCommand within a function, which waits for completion and
    returns the result as (ok (bool), output-text).
    In addition to the command to be executed it is also necessary to pass
    the callback for fetching the password, unless this has been supplied
    to a previous call, or is known to be unnecessary (unlikely, but possible).
    The cwd parameter allows the current working directory to be changed
    for the call.
    """
    if not _rc:
        init_rootrun(pwget)
    return _rc.call(cmd, cwd)