# -*- coding: utf-8 -*-
#
##########################################################################
# python-pyeole
# Copyright © 2012 Pôle de compétences EOLE <eole@ac-dijon.fr>
#
# License CeCILL:
#  * in french: http://www.cecill.info/licences/Licence_CeCILL_V2-fr.html
#  * in english http://www.cecill.info/licences/Licence_CeCILL_V2-en.html
##########################################################################
"""
Execute child program, test external TCP/UDP daemon and test container.

Execute child program
---------------------

The process module allows you to spawn new processes, get their output/error
message, and obtain their return codes.

Two function are availlable:

* system_code: just return code
* system_out: return code, output and error

Examples:

Single command:

    >>> from pyeole.process import system_code
    >>> code = system_code('ls')
    test1.txt                                       test2.txt
    >>> if code != 0:
    ...      print 'error'

Sequence of program arguments:

    >>> from pyeole.process import system_code
    >>> code = system_code(['ls', '/root'])
    test1.txt                                       test2.txt

Command with error:

    >>> from pyeole.process import system_code
    >>> code = system_code(['ls', '/notexists'])
    ls: cannot access /notexists: No such file or directory
    >>> if code != 0:
    ...     print 'error'
    error

Add stdin:

    >>> from pyeole.process import system_code
    >>> code = system_code('md5sum', stdin='eole')
    9480e1e0a13503aa15a8329f48741148  -

Change environment:

    >>> from pyeole.process import system_code
    >>> code = system_code(['ls', '/notexists'])
    ls: cannot access /notexists: No such file or directory
    >>> code = system_code(['ls', '/notexists'], env={'LC_ALL': 'fr_FR.UTF-8'})
    ls: impossible d'accéder à /notexists: Aucun fichier ou dossier de ce type

Container:

    >>> from pyeole.process import system_code
    >>> code = system_code(['ls', '/root'], container='web')
    container.txt

Get standard output/error:

    >>> from pyeole.process import system_out
    >>> code, stdout, stderr = system_out('ls')
    >>> if code != 0:
    ...     print 'error:', code, stdout, stderr
    >>>
    >>> code, stdout, stderr = system_out(['ls', '/notexists'])
    >>> if code != 0:
    ...     print 'error:', code, stdout, stderr
    error: 2  ls: cannot access /notexists: No such file or directory

Testing container
-----------------

The process module allows you to test if LXC container is started and if
SSH server is available.

Examples:

    >>> from pyeole.process import test_container
    >>> test_container('web')
    True

    >>> from pyeole.process import creole_test_container
    >>> from creole.client import CreoleClient
    >>> creole_client = CreoleClient()
    >>> container = creole_client.get_container('web')
    >>> creole_test_container(container)
    True
"""

import threading
import sys

from time import sleep
from sys import stdout as sys_stdout
from subprocess import Popen, PIPE
from shlex import quote
from pty import openpty
from os import close, read
from select import select

from shutil import which

from pyeole.decorator import deprecated
from pyeole.common import get_current_column

from creole.config import VIRTMASTER
from creole.client import CreoleClient

if sys.version_info[0] >= 3:
    import queue as Queue
else:
    import Queue


creole_client = CreoleClient()

def _gen_container_cmd(cmd, container, context, env, pty):
    """
    generate command line for container environment

    for parameter, see system_code or system_out
    :return: a sequence of program arguments
    """
    if container['name'] == VIRTMASTER:
        return cmd
    if context == False:
        if 'chroot' in container and container['chroot'] not in ['', None]:
            cmd = ['chroot', container['chroot']] + cmd
        elif container['path'] not in ['', None]:
            cmd = ['chroot', container['path']] + cmd
    else:
        if container['ip'] != '127.0.0.1':
            if not creole_test_container(container):
                raise Exception('Conteneur {0} inaccessible'.format(
                        container['name']))
            escaped_cmd = []
            if env is not None:
                for key, value in list(env.items()):
                    escaped_cmd.append('%s=%s'%(key, quote(value)))
            # échappement des doubles-quotes et des antislashes pour
            # #757 et #1439
            for elt in cmd:
                escaped_cmd.append(quote(elt))
            cmd = ['/usr/bin/ssh']
            if pty:
                cmd.append('-t')
            cmd.extend(['-q', '-o', 'LogLevel=ERROR',
                        '-o', 'StrictHostKeyChecking=no',
                        'root@%s' % container['name'] ])  # name as used in ssh_config
            cmd.extend(escaped_cmd)
    return cmd

class progressCmd(threading.Thread):
    def __init__(self, queue, cmd, env, stdin, stdout, stderr):
        threading.Thread.__init__(self)
        self.queue = queue
        self.cmd = cmd
        self.env = env
        self.stdin = stdin
        self.stdout = stdout
        self.stderr = stderr

    def run(self):
        process = Popen(self.cmd, stdin=PIPE, stdout=self.stdout, stderr=self.stderr, env=self.env)
        stdout_output, stderr_output = process.communicate(self.stdin)
        self.queue.put((process.returncode, stdout_output, stderr_output))


def _service(cmd, stdin, container, context, env, pty, stdout, stderr, progress=False, log=None, to_str=True):
    """
    generic process method

    use same parameter as system_out and system_code plus:
    :param stdout: standard output
    :param stderr: standard error
    :return: a tuple (child return code, standard output, standard error)
    """
    cmd = _gen_container_cmd(cmd, container, context, env, pty)
    if sys.version_info[0] >= 3 and  isinstance(stdin, str):
        stdin = stdin.encode()
    # fix to avoid "IOError: [Errno 9] Bad file descriptor" (EAD)
    if sys_stdout.mode == 'wb' and stdout == None:
        print("Sortie binaire détectée => affichage masqué")
        stdout = PIPE
        stderr = PIPE
    if pty and not progress:
        master, slave= openpty()
        if stdin != None:
            stdin_fd = PIPE
        else:
            stdin_fd = master
        process = Popen(cmd, stdin=stdin_fd, stdout=slave, stderr=slave,
                env=env)
        process.communicate(stdin)
        #from http://stackoverflow.com/questions/12419198/python-subprocess-readlines-hangs/12471855#12471855
        timeout = .04
        stdout_output = ''
        while 1:
            ready, _, _ = select([master], [], [], timeout)
            if ready:
                data = read(master, 512)
                if not data:
                    break
                if stdout is None:
                    sys_stdout.write(data)
                    sys_stdout.flush()
                else:
                    stdout_output += data
            elif process.poll() is not None: # select timeout
                break # proc exited
        # stderr and stdout are mixed in the same pipe
        stderr_output = stdout_output
        close(slave)
        close(master)
        process.wait()
        returncode = process.returncode
    elif progress:
        """ !! Not usable with pty
        """
        queue = Queue.Queue()
        thread = progressCmd(queue, cmd, env, stdin, stdout, stderr)
        thread.start()
        i = 0
        while True:
            sleep(0.25)
            if (i%4) == 0:
              sys_stdout.write('\b\b/]')
            elif (i%4) == 1:
              sys_stdout.write('\b\b-]')
            elif (i%4) == 2:
              sys_stdout.write('\b\b\\]')
            elif (i%4) == 3:
              sys_stdout.write('\b\b|]')

            sys_stdout.flush()
            sleep(0.2)
            i+=1
            if not thread.is_alive():
                break

        stderr_output, stdout_output = '', ''
        returncode, stdout_output, stderr_output = queue.get()
        #efface la boite
        sys_stdout.write("\b\b\b   \n")
    else:
        process = Popen(cmd, stdin=PIPE, stdout=stdout, stderr=stderr, env=env)
        stdout_output, stderr_output = process.communicate(stdin)
        returncode = process.returncode
    if to_str and sys.version_info[0] >= 3:
        stdout_output = stdout_output.decode() if isinstance(stdout_output, bytes) else stdout_output
        stderr_output = stderr_output.decode() if isinstance(stderr_output, bytes) else stderr_output
    return returncode, stdout_output, stderr_output


def system_code(cmd, stdin=None, container=VIRTMASTER, context=True, env=None,
        pty=False):
    """Execute child program in a new process. stdout and stderr are display.

    :param cmd: should be a sequence of program arguments
    :param stdin: specify the executed program’s standard input
    :param container: container name
    :param context: if context is False use chroot command, use ssh otherwise
    :param env: it must be a mapping that defines the environment variables
                for the new process; these are used instead of inheriting the
                current process’ environment, which is the default behavior.
    :param pty: if True open a new pseudo-terminal
    :return: child return code
    """
    if container in [VIRTMASTER, None]:
        container = None
    else:
        container = creole_client.get_container(container)
    return creole_system_code(cmd, stdin, container, context, env, pty)

def creole_system_code(cmd, stdin=None, container=None, context=True, env=None,
        pty=False):
    """Execute child program in a new process wit container's dictionary.
    Stdout and stderr are display.

    :param cmd: should be a sequence of program arguments
    :param stdin: specify the executed program’s standard input
    :param container: Creole container's dictionary
    :param context: if context is False use chroot command, use ssh otherwise
    :param env: it must be a mapping that defines the environment variables
                for the new process; these are used instead of inheriting the
                current process’ environment, which is the default behavior.
    :param pty: if True open a new pseudo-terminal
    :return: child return code
    """
    if container == None:
        container = {'name': VIRTMASTER}
    return _service(cmd, stdin, container, context, env, pty, None, None)[0]

def system_out(cmd, stdin=None, container=VIRTMASTER, context=True, env=None,
        pty=False, to_str=True):
    """Execute child program in a new process.

    :param cmd: should be a sequence of program arguments
    :param stdin: specify the executed program’s standard input
    :param container: container name
    :param context: if context is False use chroot command, use ssh otherwise
    :param env: it must be a mapping that defines the environment variables
                for the new process; these are used instead of inheriting the
                current process’ environment, which is the default behavior.
    :param pty: if True open a new pseudo-terminal
    :return: a tuple (child return code, standard output, standard error)
    """
    if container in [VIRTMASTER, None]:
        container = None
    else:
        container = creole_client.get_container(container)
    return creole_system_out(cmd, stdin, container, context, env, pty, to_str=to_str)


def creole_system_out(cmd, stdin=None, container=None, context=True, env=None,
        pty=False, to_str=True):
    """Execute child program in a new process with container's dictionary

    :param cmd: should be a sequence of program arguments
    :param stdin: specify the executed program’s standard input
    :param container: Creole container's dictionary
    :param context: if context is False use chroot command, use ssh otherwise
    :param env: it must be a mapping that defines the environment variables
                for the new process; these are used instead of inheriting the
                current process’ environment, which is the default behavior.
    :param pty: if True open a new pseudo-terminal
    :return: a tuple (child return code, standard output, standard error)
    """
    if container == None:
        container = {'name': VIRTMASTER}
    return _service(cmd, stdin, container, context, env, pty, PIPE, PIPE, to_str=to_str)


def system_progress_out(cmd, message='Loading', container=None, logger=None, stdin=None, context=True, env=None):
    """Execute child program in a new process. Not for containers

    :param cmd: should be a sequence of program arguments
    :param stdin: specify the executed program’s standard input
    :param message: Message pattern to be printed (default "Loading [ ]")
    :param logger: A pyeole logger object
    :param context: if context is False use chroot command, use ssh otherwise
    :param env: it must be a mapping that defines the environment variables
                for the new process; these are used instead of inheriting the
                current process’ environment, which is the default behavior.
    :return: a tuple (child return code, standard output, standard error)
    """
    progress = True
    code, out, err = system_out(['/usr/bin/tput', 'kbs'])
    if code != 0:
        progress = False
        print(message)
    else:
        col = get_current_column() - 7
        msg = u'{:<{col}} [|]'.format(message, col=col)
        sys_stdout.write(msg)

    if container == None:
        container = {'name': VIRTMASTER}
    return _service(cmd, stdin, container, context, env, False, PIPE, PIPE, progress, logger)


@deprecated(u'use new API “pyeole.diagnose.test_udp()”')
def udpcheck(ip_address, port, timeout=1):
    """Old API to make UDP checks
    """
    from pyeole.diagnose import network
    return network.test_udp(ip_address, port, timeout)

@deprecated(u'use new API “pyeole.diagnose.test_tcp()”')
def tcpcheck(ip_address, port, timeout=1):
    """Old API to make TCP checks
    """
    from pyeole.diagnose import network
    return network.test_tcp(ip_address, port, timeout)

def test_container(container):
    """
    test is a container is alive

    :param container: container name
    :return: boolean
    """
    if container == VIRTMASTER:
        return True
    container = creole_client.get_container(container)
    if container == None:
        raise ValueError('Unknown container {}'.format(container))
    return creole_test_container(container)

def creole_test_container(container):
    """
    test is a container is alive with container object

    :param container: container's dictionary with path, group, ip keys
    :return: boolean
    """
    #if container is in master container
    if container['path'] == '':
        return True

    if not which('lxc-info'):
        raise SystemError(u'LXC commands not found.')

    cmd = ['/usr/bin/lxc-info', '-n', container['group'], '-s']
    if not system_out(cmd)[1].strip().endswith('RUNNING'):
        return False
    if not tcpcheck(container['ip'], '22', '2'):
        return False
    return True

