# Copyright 1999-2014. Parallels IP Holdings GmbH. All Rights Reserved.
import os
import sys
import errno
import osutil
import logging

try: import subprocess
except ImportError: import compat.subprocess as subprocess


mswindows = (sys.platform == "win32")
if mswindows:
    from win32process import CreateProcess, STARTUPINFO, NORMAL_PRIORITY_CLASS, STARTF_USESTDHANDLES, CREATE_BREAKAWAY_FROM_JOB, CREATE_NEW_PROCESS_GROUP, CREATE_NO_WINDOW
    from pywintypes import error


_logger = logging.getLogger("api.util.subproc")


class ExecuteException(Exception):
    def __init__(self, subprocess):
        self.subprocess = subprocess

    def result(self):
        return u" has been terminated by exception"

    def __unicode__(self):
        return u"Subprocess " + unicode(self.subprocess) \
            + self.result() \
            + u"\n== STDOUT ====================\n" + self.subprocess.stdout \
            + u"\n== STDERR ====================\n" + self.subprocess.stderr


class SignalException(ExecuteException):
    def __init__(self, subprocess, signal):
        ExecuteException.__init__(self, subprocess)
        self.signal = signal

    def result(self):
        return u" has been terminated by signal " + unicode(self.signal)


class NonzeroExitException(ExecuteException):
    def __init__(self, subprocess, exitcode):
        ExecuteException.__init__(self, subprocess)
        self.exitcode = exitcode

    def result(self):
        return u" was finished with exit code " + unicode(self.exitcode)

    def __str__(self):
        return self.result().encode("UTF-8")


# Stdout, stderr is a file objects used to redirect subprocess stdout and stderr.
# if stdout and stderr are not specified, variables self.stdout and self.stderr will hold all subprocess output
class BaseSubprocess:
    def __init__(self, prog, args = [], input = None, stdout = None, stderr = None, env = os.environ, keepStderr = False):
        self.__stdout_str = None
        self.__stderr_str = None
        self.__prog = osutil.fs_compliant_encode(prog)
        self.__args = [osutil.fs_compliant_encode(item) for item in args]
        self.__env = env
        if isinstance(input, unicode):
            input = input.encode("UTF-8")
        self.__input = input
        self.__stdout = stdout
        self.__stderr = stderr
        self.__pid = None
        self.__retcode = -1
        self.__finished = False
        self.__keepStderr = keepStderr
        self.__forkexec()

    def __get_cmd(self):
        return [self.__prog] + self.__args

    def get_cmd(self):
        cmd = self.__get_cmd()
        return osutil.args_separator.join(cmd)

    def __get_stdout(self):
        if self.__stdout_str == None:
            return u''
        return self.__stdout_str;

    stdout = property(__get_stdout)

    def is_stdout_readed(self):
        return self.__stdout_str != None

    def __get_stderr(self):
        if self.__stderr_str == None:
            return u''
        return self.__stderr_str;

    stderr = property(__get_stderr)

    def is_stderr_readed(self):
        return self.__stderr_str != None

    def get_pid(self):
        return self.__pid

    def kill(self, signo):
        """Kills the associated process.
        Kills the associated OS process. wait() should be used to
        collect the process death information.
        """
        if self.__finished:
            _logger.info(u"Skipped killing finished process " + unicode(self.__pid))
        else:
            _logger.info(u"Killing process " + unicode(self.__pid))
            try:
                osutil.kill(self.__pid, signo)
            except:
                pass

    def __forkexec(self):
        _stdin = None
        if self.__input is not None:
            _stdin = subprocess.PIPE
        if not self.__stdout:
            self.__stdout = subprocess.PIPE
        if not self.__stderr:
            self.__stderr = subprocess.PIPE
        self.__process = subprocess.Popen(self.__get_cmd(), stdin = _stdin , stdout = self.__stdout, stderr = self.__stderr, env = self.__env, universal_newlines = True)
        self.__pid = self.__process.pid

    def __unicode__(self):
        return u"<subprocess[%d] %s>" % ( self.__pid, repr(self.get_cmd()) )

    def __writeEncodeUtf8(self, fd, val):
        if isinstance(val, unicode):
            val = val.encode("UTF-8")
        try:
            os.write(fd, val)
        except OSError, e:
            if e.errno != errno.EPIPE:
                raise

    def __decodeUtf8(self, fd, fd_out):
        val = os.read(fd)
        try:
            val = val.decode("UTF-8")
        except UnicodeError:
            val = val.decode("Latin-1")
        try:
            os.write(fd_out, val)
        except OSError, e:
            if e.errno != errno.EPIPE:
                raise

    def __fetch_data(self):
        child_stdout, child_stderr = self.__process.communicate(self.__input)
        if child_stdout is not None:
            try:
                self.__stdout_str = child_stdout.decode("UTF-8")
            except UnicodeError:
                self.__stdout_str = child_stdout.decode("Latin-1")

        if child_stderr is not None and not self.__keepStderr:
            try:
                self.__stderr_str = child_stderr.decode("UTF-8")
            except UnicodeError:
                self.__stderr_str = child_stderr.decode("Latin-1")

    def run(self):
        self.wait()

    def runAsync(self):
        pass

    def wait(self):
        self.__fetch_data()
        self.__retcode = self.__process.wait()
        self.__finished = True
        if self.__retcode !=  0:
            raise NonzeroExitException(self, self.__retcode)

    def poll(self):
        self.__retcode = self.__process.poll()
        self.__finished = self.__retcode is not None
        if self.__retcode:
            raise NonzeroExitException(self, self.__retcode)


class Subprocess(BaseSubprocess):
    def run(self):
        _logger.info(u"Executing " + unicode(self))
        BaseSubprocess.run(self)

    def runAsync(self):
        _logger.info(u"Executing asynchronously " + unicode(self))
        BaseSubprocess.runAsync(self)

    def wait(self):
        try:
            BaseSubprocess.wait(self)
        except ExecuteException, e:
            _logger.info(u"Subprocess raised ExecuteException: " + unicode(e))
            raise
        else:
            _logger.info(u"Execution of " + unicode(self) + u" finished successfully.")


class CmdLine(object):
    def __init__(self, filename, process_handler = None, args = [], vars = os.environ, stdin = "", stdout = None, stderr = None):
        self.filename = filename
        self.args = args
        self.vars = vars.copy()
        self.stdin = stdin
        self.stdout = stdout
        self.process_handler = process_handler
        self.stderr = stderr
        self.proc = None

    def arg(self, arg):
        self.args = self.args + [arg]
        return self

    def get_cmd(self):
        if self.proc is not None:
            return osutil.fs_compliant_decode(self.proc.get_cmd())
        else:
            return None

    def var(self, varname, varvalue):
        if isinstance(varvalue, unicode):
            self.vars[varname] = varvalue.encode("UTF-8")
        else:
            self.vars[varname] = varvalue
        return self

    def spawn(self, keepStderr = False):
        self.proc = Subprocess(self.filename,
                               self.args,
                               self.stdin,
                               stdout = self.stdout,
                               stderr = self.stderr,
                               env = self.vars,
                               keepStderr = keepStderr)
        if self.process_handler:
            self.process_handler.setProcess(self.proc)
        
        self.proc.run()
        
        if not keepStderr:
            self.stderr = self.proc.stderr
        
        if self.process_handler:
            self.process_handler.unsetProcess(self.proc)
        
        return self.proc

    def __readout(self, pipereadout):
        if pipereadout:
            try:
                os.close(self.stdout)
            except OSError:
                pass
            readout = os.read(pipereadout,1024)
            while True:
                read_chunk = os.read(pipereadout,1024)
                if read_chunk == None or len(read_chunk) == 0:
                    break
                readout += read_chunk
            try:
                readout_str = readout.decode("UTF-8")
            except UnicodeError:
                readout_str = readout.decode("Latin-1")
            return readout_str

    def get_stdout(self):
        if self.proc:
            if self.proc.is_stdout_readed():
                return self.proc.stdout
            elif self.__stdoutread:
                return self.__readout(self.__stdoutread)
            else:
                return ""

    def asyncSpawn(self, keepStderr = False):
        if not self.stdout:
            self.__stdoutread, self.stdout = os.pipe()
        self.proc = Subprocess(self.filename,
                               self.args,
                               self.stdin,
                               stdout = self.stdout,
                               stderr = self.stderr,
                               env = self.vars,
                               keepStderr = keepStderr)
        if self.process_handler:
            self.process_handler.setProcess(self.proc)
        
        self.proc.runAsync()
        return self.proc


class AsyncExecuteException(Exception):
    def __init__(self, cmdline, message):
        self.cmdline = cmdline
        self.message = message

    def __unicode__(self):
        return u"AsyncCmdLine has been terminated with exception: " \
               + self.message \
               +u"\ncommand line: " \
               + self.cmdline


class AsyncCmdLine(object):
    def __init__(self, filename, args = [], vars = os.environ):
        self.filename = filename
        self.args = args
        self.vars = vars.copy()
        self.pid = None
        self._process = None

    def arg(self, arg):
        self.args = self.args + [arg]
        return self

    def __get_cmd(self):
        cmd = [self.filename]
        cmd.extend(self.args)
        return cmd

    def get_cmd(self):
        cmd = self.__get_cmd()
        return osutil.args_separator.join(cmd)

    def var(self, varname, varvalue):
        if isinstance(varvalue, unicode):
            self.vars[varname] = varvalue.encode("UTF-8")
        else:
            self.vars[varname] = varvalue
        return self

    def asyncSpawn(self):
        # Could not use subprocess module here. On Windows system it always creates subprocess with handle inheritance.
        # Use os-native services instead
        if mswindows:
            try:
                startupinfo = STARTUPINFO()
                startupinfo.dwFlags |= STARTF_USESTDHANDLES
                startupinfo.hStdInput = 0
                startupinfo.hStdOutput = 0
                startupinfo.hStdError = 0
                
                cmd = []
                for item in self.__get_cmd():
                    cmd.append('"%s"' % item)
                
                hp, ht, pid, tid = CreateProcess(None,
                                                 osutil.args_separator.join(cmd),
                                                 None, # process security
                                                 None, # thread security
                                                 0, # do not inherit handles
                                                 NORMAL_PRIORITY_CLASS | CREATE_BREAKAWAY_FROM_JOB | CREATE_NEW_PROCESS_GROUP,
                                                 self.vars,
                                                 None, # current directory
                                                 startupinfo)
            except error, e:
                _logger.debug(u"AsyncCmdLine start error: %s\nCmdLine is '%s'" % (str(e),self.get_cmd()))
                raise AsyncExecuteException(self.get_cmd(), e.strerror)
            else:
                self.pid = pid
                _logger.debug("Executing asynchronously [%s] process" % self.pid)
        else:
            try:
                self._process = subprocess.Popen(self.__get_cmd(), close_fds = True, stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = subprocess.PIPE, env = self.vars, universal_newlines = True)
            except OSError, e:
                _logger.debug(u"AsyncCmdLine start error: %s\nCmdLine is '%s'" % (str(e),self.get_cmd()))
                raise AsyncExecuteException(self.get_cmd(), e.strerror)
            else:
                self.pid = self._process.pid
                _logger.debug("Executing asynchronously [%s] process" % self.pid)

    def get_return_code(self):
        if self._process is None:
            return None
        return self._process.poll()

    def get_stderr(self):
        if self._process is None:
            return None
        return self._process.communicate()[1]
