#!/usr/bin/python
# -*- coding: utf-8 -*
# Copyright: [CUP] - See LICENSE for details.
# Authors: Zhao Minghao, Guannan Ma
"""
:description:
    shell operations related module
"""
from __future__ import print_function
import os
import sys
import time
import uuid
import tempfile
import shutil
import signal
import random
import hashlib
import platform
import warnings
import datetime
import threading
import subprocess
import cup
from cup import err
from cup import log
from cup import thread
from cup import platforms
from cup import decorators
# linux only import
if platform.system() == 'Linux':
    from cup.res import linux
    __all__ = [
        'rm', 'rmrf', 'kill',
        'is_process_used_port', 'is_port_used', 'is_proc_exist',
        'is_proc_exist', 'is_process_running',
        'contains_file', 'backup_file',
        'ShellExec'
    ]
# universal import (platform indepedent)
else:
    __all__ = [
        'contains_file', 'backup_file'
    ]
# linux functionalities {{
# pylint: disable=C0103
def rm(name):
    """
    rm the file if no exception happens.
    Will not raise exception if it fails
    """
    try:
        os.remove(name)
    except OSError as error:
        cup.log.warn("rm oserror: %s" % error)
def rmrf(fpath, safemode=True):
    """
    :param fpath:
        files/direcotry to be deleted.
    :param safemode:
        True by default. You cannot delete root / when safemode is True
    """
    @decorators.needlinux
    def _real_rmrf(fpath, safemode):
        """
        real rmrf
        """
        if safemode:
            if os.path.normpath(os.path.abspath(fpath)) == '/':
                raise err.ShellException('cannot rmtree root / under safemode')
        if os.path.isfile(fpath):
            os.unlink(fpath)
        else:
            shutil.rmtree(fpath)
    return _real_rmrf(fpath, safemode)
def is_process_running(path, name):
    """
    Judge if the executable is running by comparing /proc files.
    :platforms:
        linux only. Will raise exception if running on other platforms
    :param path:
        executable current working direcotry
    :param name:
        executable name
    :return:
        return True if the process is running. Return False otherwise.
    """
    @decorators.needlinux
    def _real_is_proc_exist(path, name):
        """
        _real_is_proc_exist
        """
        path = os.path.realpath(os.path.abspath(path))
        cmd = 'ps -ef|grep %s|grep -v "^grep "|grep -v "^vim "|grep -v "^less "|\
            grep -v "^vi "|grep -v "^cat "|grep -v "^more "|grep -v "^tail "|\
            awk \'{print $2}\'' % (name)
        ret = cup.shell.ShellExec().run(cmd, 10)
        pids = ret['stdout'].strip().split('\n')
        if len(pids) == 0 or len(pids) == 1 and len(pids[0]) == 0:
            return False
        for pid in pids:
            for sel_path in ["cwd", "exe"]:
                cmd = 'ls -l /proc/%s/%s|awk \'{print $11}\' ' % (pid, sel_path)
                ret = cup.shell.ShellExec().run(cmd, 10)
                pid_path = ret['stdout'].strip().strip()
                if pid_path.find(path) == 0:
                    # print('%s is exist: %s' % (name, path))
                    return True
        return False
    return _real_is_proc_exist(path, name)
# for compatibility. Do not delete this line:
is_proc_exist = is_process_running
def _kill_child(pid, sign):
    cmd = 'ps -ef|grep %s|grep -v grep|awk \'{print $2,$3}\'' % (pid)
    ret = cup.shell.ShellExec().run(cmd, 10)
    pids = ret['stdout'].strip().split('\n')
    for proc in pids:
        if len(proc) == 0:
            continue
        p_id = proc.split()
        if p_id[1] == pid:
            _kill_child(p_id[0], sign)
        if p_id[0] == pid:
            if len(sign) == 0:
                cup.shell.execshell('kill %s' % pid)
            elif sign == '9' or sign == '-9':
                cup.shell.execshell('kill -9 %s' % pid)
            elif sign == 'SIGSTOP' or sign == '19' or sign == '-19':
                cup.shell.execshell('kill -19 %s' % pid)
            elif sign == 'SIGCONT' or sign == '18' or sign == '-18':
                cup.shell.execshell('kill -18 %s' % pid)
            else:
                cup.log.error('sign error')
def kill(path, name, sign='', b_kill_child=False):
    """
    will judge if the process is running by calling function
    (is_process_running), then send kill signal to this process
    :param path:
        executable current working direcotry (cwd)
    :param name:
        executable name
    :param sign:
        kill sign, e.g. 9 for SIGKILL, 15 for SIGTERM
    :b_kill_child:
        kill child processes or not. False by default.
    """
    path = os.path.realpath(os.path.abspath(path))
    # path = os.path.abspath(path)
    cmd = 'ps -ef|grep %s|grep -v grep|awk \'{print $2}\'' % (name)
    ret = cup.shell.ShellExec().run(cmd, 10)
    pids = ret['stdout'].strip().split('\n')
    for pid in pids:
        cmd = 'ls -l /proc/%s/cwd|awk \'{print $11}\' ' % (pid)
        ret = cup.shell.ShellExec().run(cmd, 10)
        if ret['returncode'] != 0:
            return False
        pid_path = ret['stdout'].strip()
        if pid_path.find(path) == 0 or path.find(pid_path) == 0:
            if b_kill_child is True:
                _kill_child(pid, sign)
            if len(sign) == 0:
                cup.shell.execshell('kill %s' % pid)
            elif sign == '9' or sign == '-9':
                cup.shell.execshell('kill -9 %s' % pid)
            elif sign == 'SIGSTOP' or sign == '19' or sign == '-19':
                cup.shell.execshell('kill -19 %s' % pid)
            elif sign == 'SIGCONT' or sign == '18' or sign == '-18':
                cup.shell.execshell('kill -18 %s' % pid)
            else:
                cup.log.error('sign error')
    return True
[docs]def backup_file(srcpath, filename, dstpath, label=None):
    """
    Backup srcpath/filename to dstpath/filenamne.label.
    If label is None, cup will use time.strftime('%H:%M:S')
    :dstpath:
        will create the folder if no existence
    """
    if label is None:
        label = time.strftime('%H:%M:%S')
    if not os.path.exists(dstpath):
        os.makedirs(dstpath)
    shutil.copyfile(
        srcpath + '/' + filename, dstpath + '/' + filename + '.' + label
    ) 
def backup_folder(srcpath, foldername, dstpath, label=None):
    """
    same to backup_file except it's a FOLDER not a FILE.
    """
    if label is None:
        label = time.strftime('%H:%M:%S')
    if not os.path.exists(dstpath):
        os.makedirs(dstpath)
    os.rename(
        '%s/%s' % (srcpath, foldername),
        '%s/%s' % (dstpath, foldername + '.' + label)
    )
def is_path_contain_file(dstpath, dstfile, recursive=False, follow_link=False):
    """
    use contains_file instead. Kept still for compatibility purpose
    """
    return contains_file(dstpath, dstfile, recursive, follow_link)
[docs]def contains_file(dstpath, expected_name, recursive=False, follow_link=False):
    """
    judge if the dstfile is in dstpath
    :param dstpath:
        search path
    :param dstfile:
        file
    :param recursive:
        search recursively or not. False by default.
    :return:
        return True on success, False otherwise
    """
    path = os.path.normpath(dstpath)
    fpath = os.path.normpath(expected_name.strip())
    fullpath = '{0}/{1}'.format(path, expected_name.strip())
    fullpath = os.path.normpath(fullpath)
    if recursive:
        for (_, __, fnames) in os.walk(path, followlinks=follow_link):
            for filename in fnames:
                if filename == fpath:
                    return True
        return False
    else:
        if os.path.exists(fullpath):
            return True
        else:
            return False 
def is_port_used(port):
    """
    judge if the port is used or not (It's not 100% sure as next second, some
    other process may steal the port as soon after this function returns)
    :platform:
        linux only (netstat command used inside)
    :param port:
        expected port
    :return:
        return True if the port is used, False otherwise
    """
    @decorators.needlinux
    def __is_port_used(port):
        """internal func"""
        cmd = "netstat -nl | grep ':%s '" % (port)
        ret = cup.shell.ShellExec().run(cmd, 10)
        if 0 != ret['returncode']:
            return False
        stdout = ret['stdout'].strip()
        if 0 == len(stdout):
            return False
        else:
            return True
    return __is_port_used(port)
def is_process_used_port(process_path, port):
    """
    judge if a process is using the port
    :param process_path:
        process current working direcotry (cwd)
    :return:
        Return True if process matches
    """
    # find the pid from by port
    cmd = "netstat -nlp | grep ':%s '|awk -F ' ' '{print $7}'|\
        cut -d \"/\" -f1" % (port)
    ret = cup.shell.ShellExec().run(cmd, 10)
    if 0 != ret['returncode']:
        return False
    stdout = ret['stdout'].strip()
    if 0 == len(stdout):
        return False
    dst_pid = stdout.strip()
    # check the path
    path = os.path.abspath(process_path)
    for sel_path in ['exe', 'cwd']:
        cmd = 'ls -l /proc/%s/%s|awk \'{print $11}\' ' % (dst_pid, sel_path)
        ret = cup.shell.ShellExec().run(cmd, 10)
        pid_path = ret['stdout'].strip().strip()
        if 0 == pid_path.find(path):
            return True
    return False
class Asynccontent(object):
    """
    make a Argcontent to async_run u have to del it after using it
    """
    def __init__(self):
        self.cmd = None
        self.timeout = None
        self.pid = None
        self.ret = {
            'stdout': None,
            'stderr': None,
            'returncode': 0
        }
        self.child_list = []
        self.cmdthd = None
        self.monitorthd = None
        self.subproc = None
        self.tempscript = None
class ShellExec(object):  # pylint: disable=R0903
    """
    For shell command execution.
    ::
        from cup import shell
        shellexec = shell.ShellExec()
        # timeout=None will block the execution until it finishes
        shellexec.run('/bin/ls', timeout=None)
        # timeout>=0 will open non-blocking mode
        # The process will be killed if the cmd timeouts
        shellexec.run(cmd='/bin/ls', timeout=100)
    """
    def __init__(self, tmpdir='/tmp/'):
        """
        :param tmpdir:
            shellexec will use tmpdir to handle temp files
        """
        self._subpro = None
        self._subpro_data = None
        self._tmpdir = tmpdir
        self._tmpprefix = 'cup.shell.{0}'.format(uuid.uuid4())
    @classmethod
    def kill_all_process(cls, async_content):
        """
        to kill all process
        """
        for pid in async_content.child_list:
            os.kill(pid, signal.SIGKILL)
    @classmethod
    def which(cls, pgm):
        """get executable"""
        if os.path.exists(pgm) and os.access(pgm, os.X_OK):
            return pgm
        path = os.getenv('PATH')
        for fpath in path.split(os.path.pathsep):
            fpath = os.path.join(fpath, pgm)
            if os.path.exists(fpath) and os.access(fpath, os.X_OK):
                return fpath
    @classmethod
    def get_async_run_status(cls, async_content):
        """
        get the process status of executing async cmd
        :return:
            None if the process has finished.
            Otherwise, return a object of linux.Process(async_pid)
        """
        try:
            async_process = linux.Process(async_content.pid)
            res = async_process.get_process_status()
        except err.NoSuchProcess:
            res = None
        return res
    @classmethod
    def get_async_run_res(cls, async_content):
        """
        if the process is still running the res shoule be None,None,0
        """
        return async_content.ret
    def async_run(self, cmd, timeout):
        """
        async_run
        return a dict {uuid:pid}
        self.argcontent{cmd,timeout,ret,cmdthd,montor}
        timeout:returncode:999
        cmd is running returncode:-999
        """
        def _signal_handle():
            """
            signal setup
            """
            signal.signal(signal.SIGPIPE, signal.SIG_DFL)
        def _target(argcontent, proc_cond):
            argcontent.tempscript = tempfile.NamedTemporaryFile(
                dir=self._tmpdir, prefix=self._tmpprefix,
                delete=True
            )
            with open(argcontent.tempscript.name, 'w+b') as fhandle:
                fhandle.write('cd {0};\n'.format(os.getcwd()))
                fhandle.write(argcontent.cmd)
            shexe = self.which('sh')
            cmds = [shexe, argcontent.tempscript.name]
            log.info(
                'to async execute {0} with script {1}'.format(
                    argcontent.cmd, cmds)
            )
            try:
                proc_cond.acquire()
                argcontent.subproc = subprocess.Popen(
                        cmds, stdout=subprocess.PIPE,
                        stderr=subprocess.PIPE,
                        preexec_fn=_signal_handle)
                proc_cond.notify()
                proc_cond.release()
            except OSError:
                proc_cond.notify()
                proc_cond.release()
                argcontent.ret['returncode'] = -1
                argcontent.ret['stderr'] = (
                    'failed to execute the cmd, plz check it out\'s'
                )
        def _monitor(start_time, argcontent):
            while(int(time.mktime(datetime.datetime.now().timetuple())) - int(start_time) <
                    int(argcontent.timeout)):
                time.sleep(1)
                if argcontent.subproc.poll() is not None:
                    self._subpro_data = argcontent.subproc.communicate()
                    argcontent.ret['returncode'] = argcontent.subproc.returncode
                    argcontent.ret['stdout'] = self._subpro_data[0]
                    argcontent.ret['stderr'] = self._subpro_data[1]
                    return
            parent = linux.Process(argcontent.subproc.pid)
            children = parent.children(True)
            ret_dict = []
            for process in children:
                ret_dict.append(process)
            argcontent.child_list = ret_dict
            str_warn = (
                'Shell "{0}"execution timout:{1}. To kill it'.format(
                    argcontent.cmd, argcontent.timeout)
            )
            self.kill_all_process(argcontent)
            argcontent.ret['returncode'] = 999
            argcontent.ret['stderr'] = str_warn
            argcontent.subproc.terminate()
        argcontent = Asynccontent()
        argcontent.cmd = cmd
        argcontent.timeout = timeout
        argcontent.ret = {
            'stdout': None,
            'stderr': None,
            'returncode': -999
        }
        proc_cond = threading.Condition(threading.Lock())
        argcontent.cmdthd = threading.Thread(
            target=_target, args=(argcontent, proc_cond))
        argcontent.cmdthd.daemon = True
        proc_cond.acquire()
        argcontent.cmdthd.start()
        start_time = int(time.mktime(datetime.datetime.now().timetuple()))
        argcontent.cmdthd.join(0.1)
        proc_cond.wait()
        proc_cond.release()
        if argcontent.subproc is not None:
            argcontent.pid = argcontent.subproc.pid
            argcontent.monitorthd = threading.Thread(target=_monitor,
                    args=(start_time, argcontent))
            argcontent.monitorthd.daemon = True
            argcontent.monitorthd.start()
            #this join should be del if i can make if quicker in Process.children
            argcontent.cmdthd.join(0.5)
        return argcontent
    def run(self, cmd, timeout):
        """
        refer to the class description
        :param timeout:
            If the cmd is not returned after [timeout] seconds, the cmd process
            will be killed. If timeout is None, will block there until the cmd
            execution returns
        :return:
            {
                'stdout' : 'Success',
                'stderr' : None,
                'returncode' : 0
            }
            returncode == 0 means success, while 999 means timeout
        E.g.
        ::
            import cup
            shelltool = cup.shell.ShellExec()
            print shelltool.run('/bin/ls', timeout=1)
        """
        def _signal_handle():
            """
            signal setup
            """
            signal.signal(signal.SIGPIPE, signal.SIG_DFL)
        def _trans_bytes(data):
            """trans bytes into unicode for python3"""
            if platforms.is_py2():
                return data
            if isinstance(data, bytes):
                try:
                    data = bytes.decode(data)
                except Exception:
                    data = 'Error to decode result'
            return data
        def _pipe_asshell(cmd):
            """
            run shell with subprocess.Popen
            """
            tempscript = tempfile.NamedTemporaryFile(
                dir=self._tmpdir, prefix=self._tmpprefix,
                delete=True
            )
            with open(tempscript.name, 'w+') as fhandle:
                fhandle.write('cd {0};\n'.format(os.getcwd()))
                fhandle.write(cmd)
            shexe = self.which('sh')
            cmds = [shexe, tempscript.name]
            log.info(
                'cup shell execute {0} with script {1}'.format(
                    cmd, cmds)
            )
            self._subpro = subprocess.Popen(
                cmds, stdout=subprocess.PIPE,
                stderr=subprocess.PIPE, preexec_fn=_signal_handle
            )
            self._subpro_data = self._subpro.communicate()
        ret = {
            'stdout': None,
            'stderr': None,
            'returncode': 0
        }
        cmdthd = threading.Thread(
            target=_pipe_asshell, args=(cmd, )
        )
        cmdthd.start()
        cmdthd.join(timeout)
        if thread.thread_alive(cmdthd):
            str_warn = (
                'Shell "%s"execution timout:%d. Killed it' % (cmd, timeout)
            )
            warnings.warn(str_warn, RuntimeWarning)
            parent = linux.Process(self._subpro.pid)
            for child in parent.children(True):
                os.kill(child, signal.SIGKILL)
            ret['returncode'] = 999
            ret['stderr'] = str_warn
            self._subpro.terminate()
        else:
            self._subpro.wait()
            times = 0
            while self._subpro.returncode is None and times < 10:
                time.sleep(1)
                times += 1
            ret['returncode'] = self._subpro.returncode
            assert type(self._subpro_data) == tuple, \
                'self._subpro_data should be a tuple'
            ret['stdout'] = _trans_bytes(self._subpro_data[0])
            ret['stderr'] = _trans_bytes(self._subpro_data[1])
        return ret
def _do_execshell(cmd, b_printcmd=True, timeout=None):
    """
    do execshell
    """
    if timeout is not None and timeout < 0:
        raise cup.err.ShellException(
            'timeout should be None or >= 0'
        )
    if b_printcmd is True:
        print('To exec cmd:{0}'.format(cmd))
    shellexec = ShellExec()
    return shellexec.run(cmd, timeout)
def execshell(cmd, b_printcmd=True, timeout=None):
    """
    执行shell命令,返回returncode
    """
    return _do_execshell(
        cmd, b_printcmd=b_printcmd, timeout=timeout)['returncode']
def execshell_withpipe(cmd):
    """
    Deprecated. Use ShellExec instead
    """
    res = os.popen(cmd)
    return res
def execshell_withpipe_ex(cmd, b_printcmd=True):
    """
    Deprecated. Recommand using ShellExec.
    """
    strfile = '/tmp/%s.%d.%d' % (
        'shell_env.py', int(os.getpid()), random.randint(100000, 999999)
    )
    os.mknod(strfile)
    cmd = cmd + ' 1>' + strfile + ' 2>/dev/null'
    os.system(cmd)
    if True == b_printcmd:
        print(cmd)
    fphandle = open(strfile, 'r')
    lines = fphandle.readlines()
    fphandle.close()
    os.unlink(strfile)
    return lines
def execshell_withpipe_str(cmd, b_printcmd=True):
    """
    Deprecated. Recommand using ShellExec.
    """
    return ''.join(execshell_withpipe_ex(cmd, b_printcmd))
def execshell_withpipe_exwitherr(cmd, b_printcmd=True):
    """
    Deprecated. Recommand using ShellExec.
    """
    strfile = '/tmp/%s.%d.%d' % (
        'shell_env.py', int(os.getpid()), random.randint(100000, 999999)
    )
    cmd = cmd + ' >' + strfile
    cmd = cmd + ' 2>&1'
    os.system(cmd)
    if b_printcmd:
        print(cmd)
    fhandle = open(strfile, 'r')
    lines = fhandle.readlines()
    fhandle.close()
    os.unlink(strfile)
    return lines
def is_proc_alive(procname, is_whole_word=False, is_server_tag=False, filters=False):
    """
    Deprecated. Recommand using cup.oper.is_proc_exist
    """
    # print procName
    if is_whole_word:
        cmd = "ps -ef|grep -w '%s'$ |grep -v grep" % procname
    else:
        cmd = "ps -ef|grep -w '%s' |grep -v grep" % procname
    if is_server_tag:
        cmd += '|grep -vwE "vim |less |vi |tail |cat |more "'
    if filters:
        if isinstance(filters, str):
            cmd += "|grep -v '%s'" % filters
        elif isinstance(filters, list):
            for _, task in enumerate(filters):
                cmd += "|grep -v '%s'" % task
    cmd += '|wc -l'
    rev = execshell_withpipe_str(cmd, False)
    if int(rev) > 0:
        return True
    else:
        return False
def forkexe_shell(cmd):
    """
    fork a new process to execute cmd (os.system(cmd))
    """
    try:
        pid = os.fork()
        if pid > 0:
            return
    except OSError:
        sys.exit(1)
    # os.chdir("/")
    os.setsid()
    # os.umask(0)
    try:
        pid = os.fork()
        if pid > 0:
            sys.exit(0)
    except OSError:
        sys.exit(1)
    os.system(cmd)
def md5file(filename):
    """
    compute md5 hex value of a file, return with a string (hex-value)
    """
    if os.path.exists(filename) is False:
        raise IOError('No such file: %s' % filename)
    with open(filename, 'rb') as fhandle:
        md5obj = hashlib.md5()
        while True:
            strtmp = fhandle.read(131072)  # read 128k one time
            if len(strtmp) <= 0:
                break
            if isinstance(strtmp, unicode):
                md5obj.update(strtmp.encode('utf-8'))
            else:
                md5obj.update(strtmp)
    return md5obj.hexdigest()
def kill9_byname(strname):
    """
    kill -9 process by name
    """
    fd_pid = os.popen("ps -ef | grep -v grep |grep %s \
            |awk '{print $2}'" % (strname))
    pids = fd_pid.read().strip().split('\n')
    fd_pid.close()
    for pid in pids:
        os.system("kill -9 %s" % (pid))
def kill_byname(strname):
    """
    kill process by name
    """
    fd_pid = os.popen("ps -ef | grep -v grep |grep %s \
            |awk '{print $2}'" % (strname))
    pids = fd_pid.read().strip().split('\n')
    fd_pid.close()
    for pid in pids:
        os.system("kill -s SIGKILL %s" % (pid))
def del_if_exist(path, safemode=True):
    """
    delete the path if it exists, cannot delete root / under safemode
    """
    if safemode and path == '/':
        raise IOError('Cannot delete root path /')
    if os.path.lexists(path) is False:
        return -1
    if os.path.isdir(path):
        shutil.rmtree(path)
    elif os.path.isfile(path) or os.path.islink(path):
        os.unlink(path)
    else:
        raise IOError('Does not support deleting the type 4 the path')
def rmtree(path, ignore_errors=False, onerror=None, safemode=True):
    """
    safe rmtree.
    safemode, by default is True, which forbids:
    1. not allowing rmtree root "/"
    """
    if safemode:
        if os.path.normpath(os.path.abspath(path)) == '/':
            raise err.ShellException('cannot rmtree root / under safemode')
    if os.path.isfile(path):
        return os.unlink(path)
    else:
        return shutil.rmtree(path, ignore_errors, onerror)
def shell_diff(srcfile, dstfile):
    """
    shell diff two files, return 0 if it's the same.
    """
    cmd = 'diff %s %s' % (srcfile, dstfile)
    return os.system(cmd)
def get_pid(process_path, grep_string):
    """
    will return immediately after find the pid which matches
    1. ps -ef|grep %s|grep -v grep|grep -vE "^[vim|less|vi|tail|cat|more] "
    '|awk '{print $2}'
    2. workdir is the same as ${process_path}
    :param process_path:
        process that runs on
    :param grep_string:
        ps -ef|grep ${grep_string}
    :return:
        return None if not found. Otherwise, return the pid
    """
    cmd = (
        'ps -ef|grep \'%s\'|grep -v grep|grep -vwE "vim |less |vi |tail |cat |more "'
        '|awk \'{print $2}\''
    ) % (grep_string)
    ret = cup.shell.ShellExec().run(cmd, 10)
    pids = ret['stdout'].strip().split('\n')
    if len(pids) == 0 or len(pids) == 1 and len(pids[0]) == 0:
        return None
    for pid in pids:
        for sel_path in ["cwd", "exe"]:
            cmd = 'ls -l /proc/%s/%s|awk \'{print $11}\' ' % (pid, sel_path)
            ret = cup.shell.ShellExec().run(cmd, 10)
            pid_path = ret['stdout'].strip().strip()
            if pid_path.find(process_path) == 0:
                return pid
    return None
# end linux functionalities }}
# vi:set tw=0 ts=4 sw=4 nowrap fdm=indent