
from __future__ import absolute_import
from __future__ import division

import distutils.spawn
import errno
import io
import logging
import os
import re
import select
import time

from vdsm.common import constants
from vdsm.common import errors
from vdsm.common import osutils
from vdsm.common.compat import subprocess
from vdsm.common.config import config
from vdsm.common.password import ProtectedPassword
from vdsm.common.time import monotonic_time

log = logging.getLogger("procutils")

OUT = "out"
ERR = "err"

_ANY_CPU = ["0-%d" % (os.sysconf('SC_NPROCESSORS_CONF') - 1)]
_SUDO_NON_INTERACTIVE_FLAG = "-n"
_USING_CPU_AFFINITY = config.get('vars', 'cpu_affinity') != ""


class CommandPath(object):
    def __init__(self, name, *args, **kwargs):
        self.name = name
        self.paths = args
        self._cmd = None
        self._search_path = kwargs.get('search_path', True)

    @property
    def cmd(self):
        if not self._cmd:
            for path in self.paths:
                if os.path.exists(path):
                    self._cmd = path
                    break
            else:
                if self._search_path:
                    self._cmd = distutils.spawn.find_executable(self.name)
                if self._cmd is None:
                    raise OSError(os.errno.ENOENT,
                                  os.strerror(os.errno.ENOENT) + ': ' +
                                  self.name)
        return self._cmd

    def __repr__(self):
        return str(self.cmd)

    def __str__(self):
        return str(self.cmd)

    def __unicode__(self):
        return unicode(self.cmd)


def command_log_line(args, cwd=None):
    return "{0} (cwd {1})".format(_list2cmdline(args), cwd)


def retcode_log_line(code, err=None):
    result = "SUCCESS" if code == 0 else "FAILED"
    return "{0}: <err> = {1!r}; <rc> = {2!r}".format(result, err, code)


def _list2cmdline(args):
    parts = []
    for arg in args:
        if isinstance(arg, ProtectedPassword):
            arg = str(arg)
        if _needs_quoting(arg) or arg == '':
            arg = "'" + arg.replace("'", r"'\''") + "'"
        parts.append(arg)
    return ' '.join(parts)


_needs_quoting = re.compile(r'[^A-Za-z0-9_%+,\-./:=@]').search


def exec_cmd(cmd, env=None):
    logging.debug(command_log_line(cmd))

    p = subprocess.Popen(
        cmd, close_fds=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
        env=env)

    out, err = p.communicate()

    logging.debug(retcode_log_line(p.returncode, err=err))

    return p.returncode, out, err


class Error(errors.Base):
    msg = ("Command {self.cmd} failed with rc={self.rc} out={self.out!r} "
           "err={self.err!r}")

    def __init__(self, cmd, rc, out, err):
        self.cmd = cmd
        self.rc = rc
        self.out = out
        self.err = err


class TimeoutExpired(errors.Base):
    msg = "Timeout waiting for process pid={self.pid}"

    def __init__(self, pid):
        self.pid = pid


def receive(p, timeout=None, bufsize=io.DEFAULT_BUFFER_SIZE):
    if timeout is not None:
        deadline = monotonic_time() + timeout
        remaining = timeout
    else:
        deadline = None
        remaining = None

    fds = {}
    if p.stdout:
        fds[p.stdout.fileno()] = OUT
    if p.stderr:
        fds[p.stderr.fileno()] = ERR

    if fds:
        poller = select.poll()
        for fd in fds:
            poller.register(fd, select.POLLIN)

        def discard(fd):
            if fd in fds:
                del fds[fd]
                poller.unregister(fd)

    while fds:
        log.debug("Waiting for process (pid=%d, remaining=%s)",
                  p.pid, remaining)
        remaining_msec = remaining * 1000 if deadline else None
        try:
            ready = poller.poll(remaining_msec)
        except select.error as e:
            if e[0] != errno.EINTR:
                raise
            log.debug("Polling process (pid=%d) interrupted", p.pid)
        else:
            for fd, mode in ready:
                if mode & select.POLLIN:
                    data = osutils.uninterruptible(os.read, fd, bufsize)
                    if not data:
                        log.debug("Fd %d closed, unregistering", fd)
                        discard(fd)
                        continue
                    yield fds[fd], data
                else:
                    log.debug("Fd %d hangup/error, unregistering", fd)
                    discard(fd)
        if deadline:
            remaining = deadline - monotonic_time()
            if remaining <= 0:
                raise TimeoutExpired(p.pid)

    _wait(p, deadline)


def _wait(p, deadline=None):
    log.debug("Waiting for process (pid=%d)", p.pid)
    if deadline is None:
        p.wait()
    else:
        timeout = 1.0 / 256
        while p.poll() is None:
            remaining = deadline - monotonic_time()
            if remaining <= 0:
                raise TimeoutExpired(p.pid)
            time.sleep(min(timeout, remaining))
            if timeout < 1.0:
                timeout *= 2
    log.debug("Process (pid=%d) terminated", p.pid)


def wrap_command(command, with_ioclass=None, ioclassdata=None,
                 with_nice=None, with_setsid=False, with_sudo=False,
                 reset_cpu_affinity=True):
    if with_ioclass is not None:
        command = ionice(command, ioclass=with_ioclass,
                         ioclassdata=ioclassdata)

    if with_nice is not None:
        command = nice(command, nice=with_nice)

    if with_setsid:
        command = setsid(command)

    if with_sudo:
        command = sudo(command)


    if reset_cpu_affinity and _USING_CPU_AFFINITY:
        command = taskset(command, _ANY_CPU)

    return command


def nice(cmd, nice):
    command = [constants.EXT_NICE, '-n', str(nice)]
    command.extend(cmd)
    return command


def ionice(cmd, ioclass, ioclassdata=None):
    command = [constants.EXT_IONICE, '-c', str(ioclass)]
    if ioclassdata is not None:
        command.extend(('-n', str(ioclassdata)))
    command.extend(cmd)
    return command


def setsid(cmd):
    command = [constants.EXT_SETSID]
    command.extend(cmd)
    return command


def sudo(cmd):
    if os.geteuid() == 0:
        return cmd
    command = [constants.EXT_SUDO, _SUDO_NON_INTERACTIVE_FLAG]
    command.extend(cmd)
    return command


def taskset(cmd, cpu_list):
    command = [constants.EXT_TASKSET, "--cpu-list", ",".join(cpu_list)]
    command.extend(cmd)
    return command


def prlimit(cmd, cpu_time=None, address_space=None):
    command = [constants.EXT_PRLIMIT]
    if cpu_time:
        command.append("--cpu=%d" % cpu_time)
    if address_space:
        command.append("--as=%d" % address_space)
    command.extend(cmd)
    return command
