text: # -*- coding:UTF-8 -*-
  #
  # Tencent is pleased to support the open source community by making QTA available.
  # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved.
  # Licensed under the BSD 3-Clause License (the "License"); you may not use this 
  # file except in compliance with the License. You may obtain a copy of the License at
  # 
  # https://opensource.org/licenses/BSD-3-Clause
  # 
  # Unless required by applicable law or agreed to in writing, software distributed 
  # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS
  # OF ANY KIND, either express or implied. See the License for the specific language
  # governing permissions and limitations under the License.
  #
  
  '''封装ADB功能
  '''
  
  from __future__ import print_function
  import os
  import sys
  import time
  import subprocess
  import threading
  import re
  from pkg_resources import iter_entry_points
  
  from adbclient import ADBClient
  from util import Singleton, Deprecated, logger, ThreadEx, TimeoutError, InstallPackageFailedError, PermissionError, is_mutibyte_string
  
  try:
      import _strptime  # time.strptime() is not thread-safed, so import _strptime first, otherwise it raises an AttributeError: _strptime_time
  except:
      pass
  cur_path = os.path.dirname(os.path.abspath(__file__))
  
  def get_adb_path():
      if sys.platform == 'win32':
          sep = ';'
          file_name = 'adb.exe'
      else:
          sep = ':'
          file_name = 'adb'
          
      for root in os.environ.get('PATH').split(sep):
          adb_path = os.path.join(root, file_name)
          if os.path.exists(adb_path):  # 优先使用环境变量中指定的 adb
              return adb_path
          
      return os.path.join(cur_path, 'tools', 'adb', sys.platform, file_name)
  
  adb_path = get_adb_path()
      
  def is_adb_server_opend():
      '''判断ADB Server是否开启
      '''
      import socket
      sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
      try:
          sock.bind(('localhost', 5037))
          sock.close()
          return False
      except:
          return True
  
  class EnumRootState(object):
      '''设备Root状态
      '''
      Unknown = 0  # 未知
      NonRoot = 1  # 非Root
      AdbdRoot = 2  # adbd以Root权限执行（执行adb root后）
      SuRoot = 3  # 使用su进入Root
  
  class IADBBackend(object):
      '''ADBBackend接口定义
      '''
      
      @staticmethod
      def list_device():
          '''枚举设备列表
          '''
          pass
  
      @staticmethod
      def open_device(name):
          '''打开指定设备
  
          :param name: 设备名称
          :type  name: str
          :return: IADBBackend实例
          '''
          pass
  
      @property
      def device_host(self):
          '''设备主机
          '''
          pass
  
      @property
      def device_name(self):
          '''设备名
          '''
          pass
  
      def run_adb_cmd(self, cmd, *args, **kwargs):
          '''执行adb命令
          '''
          pass
          
  
  class LocalADBBackend(IADBBackend):
      '''本地ADBBackend
      '''
      
      @staticmethod
      def start():
          if is_adb_server_opend(): return False
          subprocess.call([adb_path, 'start-server'])
          return True
  
      @staticmethod
      def close():
          subprocess.call([adb_path, 'kill-server'])
      
      @staticmethod
      def list_device(device_host='127.0.0.1'):
          '''枚举设备列表
          '''
          if not is_adb_server_opend(): return []
          result = ADBClient.get_client(device_host).call('devices', retry_count=3)[0]
          result = result.split('\n')
          device_list = []
          for device in result:
              if len(device) <= 1 or not '\t' in device: continue
              device_name, status = device.split('\t')
              if status != 'device': continue
              device_list.append(device_name)
          return device_list
  
      @staticmethod
      def open_device(name):
          '''打开指定设备
  
          :param name: 设备名称
          :type  name: str
          :return: IADBBackend实例
          '''
          device_host = '127.0.0.1'
          if ':' in name:
              pattern = re.compile(r'^\d{4:5}$')
              pos = name.find(':')
              hostname = name[:pos]
              if pattern.match(name[pos + 1:]):
                  # adb connect device
                  name = name[pos + 1:]
                  device_host = hostname
          
          if name not in LocalADBBackend.list_device(device_host):
              raise RuntimeError('Device %s not exist in host %s' % (name, device_host))
  
          return LocalADBBackend(device_host, name)
  
      def __init__(self, device_host, device_name, port=5037):
          self._device_host = device_host
          self._device_host_port = port
          self._device_name = device_name
          self._adb_client = ADBClient.get_client(self._device_host, self._device_host_port)
  
      @property
      def device_host(self):
          '''设备主机
          '''
          return self._device_host
  
      @property
      def device_name(self):
          '''设备名
          '''
          return self._device_name
  
      def run_adb_cmd(self, cmd, *args, **kwargs):
          '''执行adb命令
          '''
          timeout = kwargs.pop('timeout')
          sync = kwargs.pop('sync')
          return self._adb_client.call(cmd, self._device_name, *args, sync=sync, retry_count=1, timeout=timeout)
          
  
  def static_result(func):
      '''固定返回结果函数
      '''
      def _wrap_func(self):
          attr = '_%s_result' % func.__name__
          if not hasattr(self, attr):
              result = func(self)
              setattr(self, attr, result)
          return getattr(self, attr)
      return _wrap_func
  
  
  class ADB(object):
      '''封装ADB功能
      '''
      armeabi = 'armeabi'
      x86 = 'x86'
  
      connect_timeout = 300  # 连接设备的超时时间
  
      def __init__(self, backend):
          self._backend = backend
          self._device_name = self._backend.device_name
          self._root_state = EnumRootState.Unknown
          
          self._need_quote = None  # 执行shell命令时有些手机需要引号，有些不需要
          self._log_filter_thread_list = []  # 不打印log的线程id列表
          self._shell_prefix = None  # 有些设备上会有固定输出
          self._logcat_callbacks = []
          self._newline = None  # 不同手机使用的换行会不同
          
      @property
      def device_host(self):
          '''设备主机名
          '''
          return self._backend.device_host
      
      @property
      def device_name(self):
          '''设备名
          '''
          return self._backend.device_name
  
      def add_no_log_thread(self, thread):
          '''添加线程到不打印日志线程列表
          '''
          if not thread.ident in self._log_filter_thread_list:
              self._log_filter_thread_list.append(thread.ident)
      
      def remove_no_log_thread(self, thread):
          '''移除不打印日志线程列表中指定线程
          '''
          if thread.ident in self._log_filter_thread_list:
              self._log_filter_thread_list.remove(thread.ident)
              
      def run_adb_cmd(self, cmd, *args, **kwargs):
          '''执行adb命令
          '''
          retry_count = 3  # 默认最多重试3次
          if 'retry_count' in kwargs: retry_count = kwargs.pop('retry_count')
          timeout = 20
          if 'timeout' in kwargs: timeout = kwargs.pop('timeout')
          sync = True
          if 'sync' in kwargs: sync = kwargs.pop('sync')
          
          for _ in range(retry_count):
              if not threading.current_thread().ident in self._log_filter_thread_list: 
                  logger.info('adb %s:%s %s %s' % (self._backend.device_host, self._backend.device_name, cmd, ' '.join(args)))
              time0 = time.clock()
              try:
                  result = self._backend.run_adb_cmd(cmd, *args, sync=sync, timeout=timeout, **kwargs)
              except Exception as e:
                  import traceback
                  traceback.print_exc()
                  logger.warn('exec adb %s failed: %s' % (cmd, e))
                  continue
                  
              if not isinstance(result, tuple): return result
              if not threading.current_thread().ident in self._log_filter_thread_list: 
                  logger.info('执行ADB命令耗时：%s' % (time.clock() - time0))
              out, err = result
  
              if err:
                  if 'error: device not found' in err:
                      self.run_adb_cmd('wait-for-device', retry_count=1, timeout=self.connect_timeout)  # 等待设备连接正常
                      return self.run_adb_cmd(cmd, *args, **kwargs)
                  return err
              if isinstance(out, basestring): out = out.strip()
              return out
  
      def run_shell_cmd(self, cmd_line, root=False, **kwds):
          '''运行shell命令
          
          :param cmd_line: 要运行的命令行
          :param root: 是否使用root权限
          '''
          if not self._newline:
              result = self.run_adb_cmd('shell', 'echo "1\n2"')
              if '\r\n' in result:
                  self._newline = '\r\n'
              else:
                  self._newline = '\n'
                  
          def _handle_result(result):
              if not isinstance(result, (str, unicode)): return result
              if self._newline != '\n':
                  result = result.replace(self._newline, '\n')
                  
              if self._shell_prefix != None and self._shell_prefix > 0:
                  result = '\n'.join(result.split('\n')[self._shell_prefix:])
              if result.startswith('WARNING: linker:'):
                  # 虚拟机上可能会有这种错误：WARNING: linker: libdvm.so has text relocations. This is wasting memory and is a security risk. Please fix.
                  lines = result.split('\n')
                  idx = 1
                  while idx < len(lines):
                      if not lines[idx].startswith('WARNING: linker:'): break
                      idx += 1
                  return '\n'.join(lines[idx:]).strip()
              else:
                  return result
  
          if root:
              need_su = True
              if self._root_state == EnumRootState.Unknown: self._root_state = self.get_root_state()
              if self._root_state == EnumRootState.AdbdRoot:
                  need_su = False
              elif self._root_state == EnumRootState.NonRoot:
                  raise RuntimeError('device is not rooted')
                  
              if not need_su:
                  return self.run_shell_cmd(cmd_line, **kwds)
              if self._need_quote == None:
                  self._check_need_quote()
              if self._need_quote:
                  cmd_line = 'su -c \'%s\'' % cmd_line
              else:
                  cmd_line = 'su -c %s' % cmd_line
          return _handle_result(self.run_adb_cmd('shell', '%s' % cmd_line, **kwds))
  
      def reboot(self, _timeout=180):
          '''重启手机'''
          try:
              self.run_adb_cmd('reboot', retry_count=1, timeout=30)
          except TimeoutError:
              # 使用强杀init进程方式重启手机
              self.kill_process(1)
              time.sleep(10)  # 等待手机重启
          if _timeout > 0: self.wait_for_boot_complete(_timeout)
  
      def wait_for_boot_complete(self, _timeout=180):
          '''等待手机启动完成'''
          # 手机重启完后 adbd Insecure 启动时会导致adb断开重连，qt4a框架己经实现了adb root权限功能，测试手机请不要安装 adbd Insecure
          import time
          print('等待手机启动完成...')
          self.run_adb_cmd('wait-for-device', timeout=_timeout)
          boot_complete = False
          attempts = 0
          wait_period = 5
          while not boot_complete and (attempts * wait_period) < _timeout:
              output = self.run_shell_cmd("getprop sys.boot_completed", retry_count=1)
              output = output.strip()
              if output == "1":
                  boot_complete = True
              else:
                  time.sleep(wait_period)
                  attempts += 1
          if not boot_complete:
              raise RuntimeError("dev.bootcomplete 标志在  %s 秒后仍未设置，手机重启失败" % _timeout)
  
      def start_logcat(self, process_list=[], params=''):
          '''运行logcat进程
          :param process_list: 要捕获日志的进程名或进程ID列表，为空则捕获所有进程
          :type process_list:  list
          '''
          if not hasattr(self, '_start_count'): self._start_count = 0
          self._start_count += 1
          if self._start_count > 1: return
          logger.debug('[ADB] start logcat')
          self.run_shell_cmd('logcat -c ' + params)  # 清除缓冲区
          if not hasattr(self, '_log_list'):
              self._log_list = []
          self._logcat_running = True
          self._log_pipe = self.run_shell_cmd('logcat -v threadtime ' + params, sync=False)
  
          # self._logcat_thread_func(process_list)
  
          self._logcat_thread = ThreadEx(target=self._logcat_thread_func, args=[process_list, params])
          self._logcat_thread.setDaemon(True)
          self._logcat_thread.start()
          self._log_filter_thread_list.append(self._logcat_thread.ident)
  
      def stop_logcat(self):
          '''停止logcat
          '''
          if not hasattr(self, '_start_count') or self._start_count <= 0: 
              logger.warn('[ADB] logcat not start')
              return
          self._start_count -= 1
          if self._start_count > 0: return
          logger.debug('[ADB] stop logcat')
          self._logcat_running = False
          if hasattr(self, '_log_pipe'):
              if self._log_pipe.poll() == None:  # 判断logcat进程是否存在
                  try:
                      self._log_pipe.terminate()
                  except WindowsError, e:
                      logger.warn('terminate logcat process failed: %s' % e)
                      
          if hasattr(self, '_logcat_thread'):
              if self._logcat_thread.ident in self._log_filter_thread_list:
                  self._log_filter_thread_list.remove(self._logcat_thread.ident)
              else:
                  logger.warn('%s not in %s' % (self._logcat_thread.ident, self._log_filter_thread_list))
  
      def get_log(self, clear=True):
          '''获取已经保存的log
          '''
          if not hasattr(self, '_log_list'): return []
          result = self._log_list
          if clear: self._log_list = []
          return result
  
      def save_log(self, save_path):
          '''保存log
          '''
          if not hasattr(self, '_log_list'): return
          log_list = self.get_log()
          for i in range(len(log_list)):
              log = log_list[i]
              if not isinstance(log, unicode):
                  # 先编码为unicode
                  for code in ['utf8', 'gbk']:
                      try:
                          log = log.decode(code)
                          break
                      except UnicodeDecodeError, e:
                          # logger.warn('decode with %s error: %s' % (code, e))
                          pass
                  else:
                      log = repr(log)
              log_list[i] = log.encode('utf8') if isinstance(log, unicode) else log
          f = open(save_path, 'w')
          f.write('\n'.join(log_list))
          f.close()
      
      def add_logcat_callback(self, callback):
          '''添加logcat回调
          '''
          if not callback in self._logcat_callbacks:
              self._logcat_callbacks.append(callback)
      
      def remove_logcat_callback(self, callback):
          '''移除logcat回调
          '''
          if callback in self._logcat_callbacks:
              self._logcat_callbacks.remove(callback)
              
      def insert_logcat(self, process_name, year, month_day, timestamp, level, tag, tid, content):
          self._log_list.append('[%s] [%s-%s %s] %s/%s(%s): %s' % (process_name,
                                                                   year, month_day, timestamp,
                                                                   level,
                                                                   tag,
                                                                   tid,
                                                                   content))
          pid = 0
          pattern = re.compile(r'^(.+)\((\d+)\)$')
          ret = pattern.match(process_name)
          if ret:
              process_name = ret.group(1)
              pid = int(ret.group(2))
          for callback in self._logcat_callbacks:
              callback(pid, process_name, '%s-%s' % (year, month_day), timestamp, level, tag, int(tid), content)
              
      def _logcat_thread_func(self, process_list, params=''):
          '''获取logcat线程
          '''
          import re
          # pattern = re.compile(r'([A-Z])/([\w|.]+)\s*\(\s*(\d+)\):.+') #标准格式
          # pattern = re.compile(r'([\d|-]+)\s+([\d|:|\.]+)\s+(\d+)\s+(\d+)\s+(\w)\s+(\S+)\s*:\s+(.+)')  # [^:]
          pattern = re.compile(r'([\d|-]+)\s+([\d|:|\.]+)\s+(\d+)\s+(\d+)\s+(\w)\s+(.*?)\s*:\s*(.*)')  # 会过滤掉只有内容和内容为空的情况：--------- beginning of /dev/log/main not match pattern；04-16 10:09:25.170  2183  2183 D AndroidRuntime:
          # Date Time PID TID Level Tag Content 
          pid_dict = {}
          filter_pid_list = []  # 没有找到匹配进程的列表
          zygote_pid = 0  # zygote进程ID
  
          while self._logcat_running:
              log = self._log_pipe.stdout.readline().strip()
              if not log:
                  if self._log_pipe.poll() != None:
                      logger.debug('logcat进程：%s 已退出' % self._log_pipe.pid)
                      # 进程已退出
                      # TODO: 解决logcat重复问题
                      if not self._logcat_running:
                          logger.info('logcat线程停止运行')
                          return
                      self._log_pipe = self.run_shell_cmd('logcat -v threadtime ' + params, sync=False)
                  else:
                      continue
  
              ret = pattern.match(log)
              if not ret: 
                  logger.info('log: %s not match pattern' % log)
                  continue
              tag = ret.group(6).strip()
              if tag in ['inject', 'dexloader', 'ActivityInspect', 'MethodHook', 'androidhook']:
                  logger.info(log)  # 测试桩日志加入到qt4a日志中
                  continue
  
              if tag in ['Web Console']:
                  if ret.group(7).startswith('[ClickListener]'):
                      logger.info(log)  # WebView的控件点击信息
                      continue
                  
              pid = int(ret.group(3))
              if pid in filter_pid_list: continue
  
              init_process_list = ['<pre-initialized>', 'zygote']
  
              if not pid in pid_dict.keys():
                  for item in self.list_process():
                      if zygote_pid == 0 and item['proc_name'] == 'zygote' and item['ppid'] == 1:
                          # zygote父进程ID为1
                          zygote_pid = item['pid']
  
                      for init_process in init_process_list:
                          if pid_dict.has_key(item['pid']) and pid_dict[item['pid']].startswith(init_process) and not item['proc_name'].startswith(init_process):
  
                              for i in range(len(self._log_list) - 1, -1, -1):
                                  # 修复之前记录的“<pre-initialized>”进程
                                  pre_process_name = '[%s(%d)]' % (init_process, item['pid'])
                                  if not pre_process_name in self._log_list[i]: continue
                                  if process_list:
                                      del_flag = True
                                      for process in process_list:
                                          if pid == process or item['proc_name'].startswith(process):
                                              # 替换为真实进程名
                                              self._log_list[i] = self._log_list[i].replace(pre_process_name, ('[%s(%d)]' % (item['proc_name'], item['pid'])))
                                              del_flag = False
                                              break
                                      if del_flag:
                                          # 不在需要记录的进程列表中
                                          del self._log_list[i]
                                  else:
                                      # 直接替换
                                      self._log_list[i] = self._log_list[i].replace(pre_process_name, ('[%s(%d)]' % (item['proc_name'], item['pid'])))
                      pid_dict[item['pid']] = item['proc_name']
  #                     if item['proc_name'] in init_process_list and item['pid'] != zygote_pid:
  #                         pid_dict[item['pid']] += '(%d)' % item['pid']
                  if not pid in pid_dict.keys():
                      filter_pid_list.append(pid)
                      continue
  
              found = False
              if not process_list:
                  found = True  # 不指定进程列表则捕获所有进程
              else:
                  for process in process_list:
                      if pid == process or (pid_dict.has_key(pid) and (pid_dict[pid].startswith(process) or pid_dict[pid].startswith('<pre-initialized>') \
                                                                       or (pid_dict[pid].startswith('zygote') and pid != zygote_pid))):  # 进程初始化中
                          found = True
                          break
  
              if found:
                  import datetime
                  if not hasattr(self, '_year'):
                      self._year = datetime.date.today().year
                  self.insert_logcat('%s(%d)' % (pid_dict.get(pid), pid), self._year, ret.group(1), ret.group(2), ret.group(5), ret.group(6), ret.group(4), ret.group(7))
      
      @static_result
      def get_root_state(self):
          '''获取Root状态
          '''
          if self.is_adbd_root(): return EnumRootState.AdbdRoot
          result = self.run_shell_cmd('su -c id')
          if 'su: not found' in result: return EnumRootState.NonRoot
          elif 'uid=0(root)' in result: return EnumRootState.SuRoot
          return EnumRootState.NonRoot
      
      @static_result
      def is_adbd_root(self):
          '''adbd是否以root权限运行
          '''
          result = self.run_shell_cmd('id')
          logger.debug('is_adbd_root: %s' % result)
          return 'uid=0(root)' in result
      
      def is_rooted(self):
          return self.get_root_state() in (EnumRootState.AdbdRoot, EnumRootState.SuRoot)
      
      def _check_need_quote(self, timeout=20):
          '''
          '''
          cmd = "su -c 'ls -l /data/data'"  # 默认方式为加引号，避免有些手机上对于存在空格的命令容易出错
          # 联想S899T上发现不加引号返回结果为空
          result = self.run_shell_cmd(cmd, timeout=timeout)
          if result.find('com.android.phone') >= 0:
              self._need_quote = True
          else:
              logger.debug(result)
              self._need_quote = False
  
  # ifndef __RELEASE__
      def _set_system_writable(self):
          '''修改system分区可写
          '''
          result = self.run_shell_cmd('mount', True)
          for line in result.split('\n'):
              if line.find('/system') >= 0:
                  block = line.split(' ')[0]
                  print(block)
                  self.run_shell_cmd('mount -o remount %s /system' % block, True)
                  return True
          return False
  # endif
  
      def forward(self, port1, port2, type='tcp'):
          '''端口转发
          :param port1: PC上的TCP端口
          :type port1:  int
          :param port2: 手机上的端口或LocalSocket地址
          :type port2:  int或String
          :param type:  手机上的端口类型
          :type type:   String，LocalSocket地址使用“localabstract”
          '''
          while 1:
              ret = self.run_adb_cmd('forward', 'tcp:%d' % (port1), '%s:%s' % (type, port2))
              if not 'cannot bind socket' in ret and not 'cannot bind to socket' in ret: return port1
              port1 += 1
      
      def remove_forward(self, port):
          '''移除指定的端口映射
          '''
          return 'cannot remove listener' in self.run_adb_cmd('forward', '--remove', 'tcp:%d' % (port))
      
      def create_tunnel(self, addr, type='tcp'):
          '''直接创建与手机中socket服务端的连接
          '''
          sock = self.run_adb_cmd('create_tunnel', '%s:%s' % (type, addr))
          if sock == '': return None
          return sock
      
      def _push_file(self, src_path, dst_path):
          '''以指定身份拷贝文件到手机中
          '''
          result = self.run_adb_cmd('push', src_path, dst_path, timeout=None)
          if 'No space left on device' in result or 'No such file or directory' in result:
              # 如果源文件不存在不会执行到这里
              raise RuntimeError('设备存储空间不足')
          return result
  
      def push_file(self, src_path, dst_path, uid=None):
          '''以指定身份拷贝文件到手机中
          '''
          if isinstance(dst_path, unicode): dst_path = dst_path.encode('utf8')
          file_size = 0
          for _ in range(3):
              file_size = os.path.getsize(src_path)  # 防止取到的文件大小不正确
              result = self._push_file(src_path, dst_path)
              if file_size == 0:
                  logger.warn('文件大小为0')
                  return result
              if ('%d' % file_size) in result:
                  try:
                      _, file_list = self.list_dir(dst_path)
                      if len(file_list) == 0:
                          logger.warn('push file failed: file not exist')
                      elif file_list[0]['size'] != file_size:
                          logger.warn('push file failed: file size error, expect %d, actual is %d' % (file_size, file_list[0]['size']))
                          self.delete_file(dst_path)
                      else:
                          logger.debug(repr(file_list[0]))
                          if uid: self.chown(dst_path, uid, uid)
                          return result
                  except RuntimeError, e:
                      err_msg = e.args[0]
                      if not isinstance(err_msg, unicode):
                          err_msg = err_msg.decode('utf8')
                      logger.warn(err_msg)
              else:
                  logger.warn('push file failed: %s' % result)
          raise RuntimeError('Push file [%d]%s to device [%r] failed: %s' % (file_size, src_path, self._device_name, result))
  
      def pull_file(self, src_path, dst_path):
          '''从手机中拉取文件
          '''
          result = self.run_adb_cmd('pull', src_path, dst_path, timeout=600)
          if 'failed to copy' in result:
              raise RuntimeError(result)
          if not 'bytes in' in result:
              logger.warn(repr(result))
              logger.debug(self.run_shell_cmd('ls -l %s' % src_path, True))
          return result
      
      @staticmethod
      def _get_package_name(apk_path):
          '''获取安装包名
          '''
          import zipfile
          from _axmlparser import AXMLPrinter
          package_name = ''
          zf = zipfile.ZipFile(apk_path, mode='r')
          for i in zf.namelist():
              if i == "AndroidManifest.xml":
                  printer = AXMLPrinter(zf.read(i))
                  package_name = printer.get_xml_obj().getElementsByTagName('manifest')[0].getAttribute('package')
                  break
          if not package_name: raise RuntimeError('获取安装包中的包名信息失败')
          return package_name
          
      def _install_apk(self, apk_path, package_name, reinstall=False):
          '''
          '''
          if self.get_sdk_version() <= 19:
              timeout = 3 * 60
          else:
              timeout = 6 * 60  # TODO: 9100安装5.0系统后安装应用超过3分钟
          cmdline = 'pm install %s %s' % ('-r' if reinstall else '', apk_path)
          ret = ''
          for i in range(3):
              # 处理一些必然会失败的情况，如方法数超标之类的问题
              try:
                  if not self.is_rooted():
                      self.run_shell_cmd('am broadcast -a startInstallMonitor')  # 通知QT4A助手开始监控应用安装
                      ret = self.run_shell_cmd(cmdline, retry_count=1, timeout=timeout)
                  else:
                      proc = self.run_shell_cmd(cmdline, True, sync=False)  # 使用root权限安装
                      time0 = time.time()
                      close_popup_count = 0
                      while time.time() - time0 < timeout:
                          if proc.poll() != None: 
                              ret = proc.communicate()[0]
                              break
                          elif time.time() - time0 > 10 and close_popup_count < 2:
                              # 有些系统上弹窗会出现很久，关掉弹窗可以避免超时
                              self.run_shell_cmd('input keyevent 4')
                              close_popup_count += 1
                          time.sleep(1)
                      else:
                          raise TimeoutError('Install package timeout')
                      
                      if not 'Success' in ret:
                          logger.warn('install with root failed: %s' % ret)
                          if not 'INSTALL_' in ret.strip().split('\n')[-1]:
                              # 权限弹窗导致的安装失败
                              ret = self.run_as('system', cmdline, retry_count=1, timeout=timeout)
                      
                  logger.debug(ret)
                  if 'Success' in ret:
                      return True, ret
                  elif i > 1 and 'INSTALL_FAILED_ALREADY_EXISTS' in ret:
                      # 出现至少一次超时，认为安装完成
                      return True, 'Success'
                  elif 'INSTALL_FAILED_ALREADY_EXISTS' in ret:
                      # 尝试覆盖安装
                      return self._install_apk(apk_path, package_name, True)
                  elif 'INSTALL_PARSE_FAILED_NO_CERTIFICATES' in ret or 'INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION' in ret:
                      if i >= 2: return False, ret
                      time.sleep(10)
                      continue
                  elif 'INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES' in ret or 'INSTALL_FAILED_DEXOPT' in ret or 'INSTALL_FAILED_UPDATE_INCOMPATIBLE' in ret:
                      # 必须卸载安装
                      if not reinstall: return False, ret
                      self.uninstall_app(package_name)
                      return self._install_apk(apk_path, package_name, False)
                  elif 'INSTALL_FAILED_INSUFFICIENT_STORAGE' in ret:
                      # 有可能是存在/data/app-lib/packagename-1目录导致的
                      for i in (1, 2):
                          dir_path = '/data/app-lib/%s-%d' % (package_name, i)
                          if 'No such file or directory' in self.run_shell_cmd('ls -l %s' % dir_path, True):
                              continue
                          else:
                              self.delete_folder(dir_path)
                              break
                      else:
                          return False, ret
                  elif 'INSTALL_FAILED_UID_CHANGED' in ret or 'INSTALL_FAILED_INTERNAL_ERROR' in ret:
                      # /data/data目录下存在文件夹没有删除
                      dir_path = '/data/data/%s' % package_name
                      for _ in range(3):
                          # 防止删除没有成功
                          self.delete_folder(dir_path)
                          if 'No such file or directory' in self.run_shell_cmd('ls -l %s' % dir_path, True): break
                      continue
                  elif 'INSTALL_FAILED_CANCELLED_BY_USER' in ret:
                      # 一般是ROM需要手动确认安装，改用system权限安装
                      ret = self.run_shell_cmd('su system %s' % cmdline, timeout=timeout)
                      if 'Success' in ret: return True, ret
                  elif 'Error: Could not access the Package Manager' in ret:
                      # 设备出现问题，等待监控程序重启设备
                      time.sleep(30)
                  else:
                      return False, ret
              except TimeoutError, e:
                  logger.warn('install app timeout: %r' % e)
          else:
              logger.warn('install app failed')
              ret = self.run_shell_cmd(cmdline, timeout=timeout)  # 改用非root权限安装
              logger.debug(ret)
              if 'Success' in ret or 'INSTALL_FAILED_ALREADY_EXISTS' in ret: return True, 'Success'
          
          return False, ret
  
      def install_apk(self, apk_path, reinstall=False):
          '''安装应用
          '''
          if not os.path.exists(apk_path):
              raise RuntimeError('APK: %s 不存在' % apk_path)
          package_name = self._get_package_name(apk_path)
  
          tmp_path = '/data/local/tmp/' + os.path.split(apk_path)[-1]
          self.push_file(apk_path, tmp_path)
          
          if not reinstall:
              self.uninstall_app(package_name)  # 先卸载，再安装
              result = self._install_apk(tmp_path, package_name, reinstall)
          else:
              result = self._install_apk(tmp_path, package_name, reinstall)
          # logger.debug(result)
  
          if result[0] == False: 
              if not 'Failure' in result[1]: 
                  # 一般这种情况都是由于adb server意外退出导致，此时安装过程还会继续
                  logger.warn('install app: %r' % result[1])
                  timeout = 30
                  time0 = time.time()
                  while time.time() - time0 < timeout:
                      # 等待应用安装完成
                      if self.get_package_path(package_name): break
                      time.sleep(1)
                  else:
                      result = self._install_apk(tmp_path, package_name, reinstall)
              else:
                  err_msg = result[1]
                  if isinstance(err_msg, unicode): err_msg = err_msg.encode('utf8')
                  if isinstance(package_name, unicode): package_name = package_name.encode('utf8')
                  raise InstallPackageFailedError('安装应用%s失败：%s' % (package_name, err_msg))
          try:
              self.delete_file('/data/local/tmp/*.apk')
          except TimeoutError:
              pass
          
      def uninstall_app(self, pkg_name):
          '''卸载应用
          '''
          result = ''
          if not self.get_package_path(pkg_name): return True
          for _ in range(5):
              try:
                  result = self.run_adb_cmd('uninstall', pkg_name, retry_count=1, timeout=30)
                  break
              except RuntimeError:
                  logger.exception('uninstall %s failed' % pkg_name)
                  time.sleep(10)
          else: raise
          logger.debug('uninstall %s result: %r' % (pkg_name, result))
          if self.is_rooted():
              # 清理卸载可能遗留的cache文件
              cpu_abi = 'arm'
              if self.get_cpu_abi() == 'x86': cpu_abi = 'x86'  # TODO: 支持64位CPU   
              self.delete_file('/data/dalvik-cache/%s/data@app@%s-*' % (cpu_abi, pkg_name))
          return 'Success' in result
  
  # ifndef __RELEASE__
      @Deprecated('uninstall_app')
      def uninstall_apk(self, pkg_name):
          '''卸载应用
          '''
          return self.uninstall_app(pkg_name)
  # endif
  
      def get_package_path(self, pkg_name):
          '''获取应用安装包路径
          '''
          for _ in range(3):
              # 为避免某些情况下获取不到应用安装包路径，重试多次
              result = self.run_shell_cmd('pm path %s' % pkg_name)
              logger.debug('get_package_path: %r' % result)
              pos = result.find('package:')
              if pos >= 0: return result[pos + 8:]
              time.sleep(1)
          return ''
      
      def get_package_version(self, pkg_name):
          '''获取应用版本
          '''
          result = self.run_shell_cmd('dumpsys package %s' % pkg_name)
          for line in result.split('\n'):
              line = line.strip()
              if line.startswith('versionName='):
                  return line[12:]
          
      def _build_intent_extra_string(self, extra):
          '''构造intent参数列表
          '''
          extra_str = ''
          for key in extra.keys():  # 指定额外参数
              p_type = ''
              if extra[key] in ['true', 'false']:
                  p_type = 'z'  # EXTRA_BOOLEAN_VALUE
              elif isinstance(extra[key], int):
                  p_type = 'i'  # EXTRA_INT_VALUE
              elif isinstance(extra[key], long):
                  p_type = 'l'  # EXTRA_LONG_VALUE
              elif isinstance(extra[key], float):
                  p_type = 'f'  # EXTRA_FLOAT_VALUE
              elif extra[key].startswith('file://'):  # EXTRA_URI_VALUE
                  p_type = 'u'
              param = '-e%s %s %s ' % (p_type, key, ('"%s"' % extra[key]) if not p_type else extra[key])
              if p_type: param = '-' + param
              extra_str += param
          if len(extra_str) > 0: extra_str = extra_str[:-1]
          return extra_str.encode('utf8')
      
      def start_activity(self, activity_name, action='', type='', data_uri='', extra={}, wait=True):
          '''打开一个Activity
          Warning: Activity not started, intent has been delivered to currently running top-most instance.
          Status: ok
          ThisTime: 0
          TotalTime: 0
          WaitTime: 2
          Complete
          '''
          if activity_name:
              activity_name = '-n %s' % activity_name
          if action != '':  # 指定Action
              action = '-a %s ' % action
          if type != '':
              type = '-t %s ' % type
          if data_uri != '':
              data_uri = '-d "%s" ' % data_uri
          extra_str = self._build_intent_extra_string(extra)
          W = ''
          if wait: W = '-W'  # 等待启动完成才返回
          # 如果/sbin/sh指向busybox，就会返回“/sbin/sh: am: not found”错误
          # 返回am找不到是因为am缺少“#!/system/bin/sh”
          command = 'am start %s %s %s%s%s%s' % (W, activity_name, action, type, data_uri, extra_str)
          if command[-1] == ' ': command = command[:-1]
          result = self.run_shell_cmd(command, timeout=15, retry_count=3)
          if 'Permission Denial' in result or (wait and (not 'Activity:' in result or not 'Complete' in result)):
              # 使用root权限运行
              if self.is_rooted():
                  result = self.run_shell_cmd(command, True, timeout=15, retry_count=3)
              else:
                  package_name = activity_name.split('/')[0].split()[1]
                  result = self.run_as(package_name, command, timeout=15, retry_count=3)
              # raise RuntimeError('打开Activity失败：\n%s' % result)
          if 'startActivityAndWait asks to run as user -2 but is calling from user 0' in result:
              command += ' --user 0'
              result = self.run_as(package_name, command, timeout=15, retry_count=3)
              logger.info('start activity command:%s' % command)
          if 'Permission Denial' in result or ('run as user -2 but is calling from user 0' in result) or (wait and not 'Complete' in result):
              raise RuntimeError('start activity failed: %s' % result)
          
          ret_dict = {}
          for line in result.split('\n'):
              if ': ' in line:
                  key, value = line.split(': ')
                  ret_dict[key] = value
          if ret_dict.has_key('Error'):
              raise RuntimeError(ret_dict['Error'])
          return ret_dict
      
      def start_service(self, service_name, extra={}):
          '''启动服务
          '''
          extra_str = self._build_intent_extra_string(extra)
          command = 'am startservice -n %s %s' % (service_name, extra_str)
          if command[-1] == ' ': command = command[:-1]
          result = self.run_shell_cmd(command, timeout=15, retry_count=3)
          if 'no service started' in result or 'java.lang.SecurityException' in result:
              raise RuntimeError('start service %s failed: %s' % (service_name, result))
                  
      def stop_service(self, service_name):
          '''停止服务
          '''
          result = self.run_shell_cmd('am stopservice -n %s' % service_name, timeout=15, retry_count=3)
          if not 'Service stopped' in result and not 'was not running' in result: raise RuntimeError('stop service failed: %s' % result)
      
      def send_broadcast(self, action, extra={}):
          '''发送广播
          
          :param action: 广播使用的ACTION
          :type  action: string
          :param extra:  额外参数
          :type  extra:  dict
          '''
          extra_str = self._build_intent_extra_string(extra)
          command = 'am broadcast -a %s %s' % (action, extra_str)
          result = self.run_shell_cmd(command)
          if not 'Broadcast completed: result=0' in result:
              raise RuntimeError('Send broadcast failed: %s' % result)
          
      def get_property(self, prop):
          '''读取属性
          '''
          return self.run_shell_cmd('getprop %s' % prop)
      
      def set_property(self, prop, value):
          '''设置属性
          '''
          self.run_shell_cmd('setprop %s %s' % (prop, value), self.is_rooted())
  
      @static_result
      def get_cpu_abi(self):
          '''获取系统的CPU架构信息
          '''
          ret = self.run_shell_cmd('getprop ro.product.cpu.abi')
          if not ret: ret = 'armeabi'  # 有些手机可能没有这个系统属性
          return ret
      
      @static_result
      def get_device_model(self):
          '''获取设备型号
          '''
          model = self.run_shell_cmd('getprop ro.product.model')
          brand = self.run_shell_cmd('getprop ro.product.brand')
          if model.find(brand) >= 0:
              return model
          return '%s %s' % (brand, model)
      
      @static_result
      def get_system_version(self):
          '''获取系统版本
          '''
          return self.run_shell_cmd('getprop ro.build.version.release')
      
      @static_result
      def get_sdk_version(self):
          '''获取SDK版本
          '''
          return int(self.run_shell_cmd('getprop ro.build.version.sdk'))
  
      def get_uid(self, app_name):
          '''获取APP的uid
          '''
          result = self.run_shell_cmd('ls -l /data/data', True)
          for line in result.split('\n'):
              items = line.split(' ')
              for item in items:
                  if not item: continue
                  if item == app_name: return items[1]
          return None
  
      def is_selinux_opened(self):
          '''selinux是否是enforcing状态
          '''
          if self.get_sdk_version() < 18: return False
          return 'Enforcing' in self.run_shell_cmd('getenforce', True)
      
      def close_selinux(self):
          '''关闭selinux
          '''
          result = self.run_shell_cmd('setenforce 0', True)
          if 'Permission denied' in result: return False
          return True
      
      def chmod(self, file_path, attr):
          '''修改文件/目录属性
          
          :param file_path: 文件/目录路径
          :type file_path:  string
          :param attr:      设置的属性值，如：777
          :type attr:       int
          '''
          def _parse(num):
              num = str(num)
              attr = ''
              su_flag = ''
              if len(num) == 4:
                  su_flag = int(num[0])
                  num = num[1:]
              for c in num:
                  c = int(c)
                  if c & 4:
                      attr += 'r'
                  else:
                      attr += '-'
                  if c & 2:
                      attr += 'w'
                  else:
                      attr += '-'
                  if c & 1:
                      attr += 'x'
                  else:
                      attr += '-'
  
              if su_flag and su_flag == 4:
                  attr = attr[:2] + 's' + attr[3:]
              return attr
  
          ret = self.run_shell_cmd('chmod %s %s' % (attr, file_path), self.is_rooted())
          dir_list, file_list = self.list_dir(file_path)
  
          if len(dir_list) == 0 and len(file_list) == 1 and file_path.endswith('/' + file_list[0]['name']):
              # 这是一个文件
              new_attr = file_list[0]['attr']
          else:
              # 目录
              dir_name = file_path.split('/')[-1]
              parent_path = '/'.join(file_path.split('/')[:-1])
              dir_list, _ = self.list_dir(parent_path)
              for dir in dir_list:
                  if dir['name'] == dir_name:
                      new_attr = dir['attr']
                      break
  
          if new_attr != _parse(attr):
              logger.warn('chmod failed: %r(%s)' % (ret, new_attr))
              return self.chmod(file_path, attr)
          return new_attr
      
      def chown(self, file_path, uid, gid):
          '''修改文件的拥有者和群组
          
          :param file_path: 文件路径
          :type file_path:  string
          :param uid:       拥有者
          :type uid:        string
          :param gid:       群组
          :type gid:        string
          '''
          self.run_shell_cmd('chown %s:%s %s' % (uid, gid, file_path), True)
          
      def mkdir(self, dir_path, mod=None):
          '''创建目录
          '''
          cmd = 'mkdir %s' % (dir_path)
          ret = self.run_shell_cmd(cmd, self.is_rooted())
  #        if not 'File exists' in ret:
  #            #加了-p参数貌似不会返回这个提示信息
          try:
              self.list_dir(dir_path)
          except RuntimeError, e:
              logger.warn('mkdir %s failed: %s(%s)' % (dir_path, ret, e))
              return self.mkdir(dir_path, mod)
          # 修改权限
          if mod != None:
              self.chmod(dir_path, mod)
  
      def list_dir(self, dir_path):
          '''列取目录
          '''
          if ' ' in dir_path: dir_path = '"%s"' % dir_path
          use_root = self.is_rooted()
          if use_root and dir_path.startswith('/sdcard') or dir_path.startswith('/storage/') or dir_path.startswith('/mnt/'):
              # 部分手机上发现用root权限访问/sdcard路径不一致
              use_root = False
          result = self.run_shell_cmd('ls -l %s' % dir_path, use_root)
          
          if 'Permission denied' in result:
              raise PermissionError(result)
          if 'No such file or directory' in result:
              raise RuntimeError('file or directory %s not exist' % dir_path)
          if 'Not a directory' in result:
              raise RuntimeError(u'%s %s' % (dir_path, result))
          
          dir_list = []
          file_list = []
  
          def _handle_name(name):
              return name.split('/')[-1]
          
          is_toybox = self.get_sdk_version() >= 24
          is_busybox = None
          # busybox格式 -rwxrwxrwx    1 shell    shell        13652 Jun  3 10:56 /data/local/tmp/qt4a/inject
          
          for line in result.split('\n'):
              items = line.split()
              if len(items) < 6: continue  # (6, 7, 9)
              if not line[0] in ('-', 'd', 'l'): continue
              
              is_dir = items[0][0] == 'd'  # 是否是目录
              is_link = items[0][0] == 'l'  # 软链
              if is_busybox == None:
                  if is_toybox:
                      item = items[5]  # 日期字段
                  else:
                      item = items[4]  # 日期字段
                      if is_dir or is_link: item = items[3]  # 目录和软链没有size字段
                  pattern = re.compile(r'\d{4}-\d{2}-\d{2}')
                  if pattern.match(item):
                      is_busybox = False
                  else:
                      is_busybox = True
  
              if not is_busybox:
                  # 防止文件名称中有空格
                  if not is_toybox:
                      if not is_dir and not is_link and len(items) > 7:
                          items[6] = line[line.find(items[6]):].strip()
                      elif is_dir and len(items) > 6:
                          items[5] = line[line.find(items[5]):].strip()
                  else:
                      if not is_dir and not is_link and len(items) > 8:
                          items[7] = line[line.find(items[7]):].strip()
                      elif is_dir and len(items) > 7:
                          items[6] = line[line.find(items[6]):].strip()
                      
              attrs = items[0]
              if attrs[0] == 'd':
                  if is_busybox: name = _handle_name(items[8])
                  elif is_toybox: name = items[7]
                  else: name = items[5]
                  dir_list.append({'name': name, 'attr': attrs[1:]})
              elif attrs[0] == '-':
                  if is_busybox:
                      name = _handle_name(items[8])
                      size = int(items[4])
                      last_modify_time = items[7]
                  elif is_toybox:
                      name = _handle_name(items[7])
                      size = int(items[4])
                      last_modify_time = time.strptime('%s %s:00' % (items[5], items[6]), "%Y-%m-%d %X")
                  else:
                      name = items[6]
                      size = int(items[3])
                      try:
                          last_modify_time = time.strptime('%s %s:00' % (items[4], items[5]), "%Y-%m-%d %X")
                      except:
                          # TODO: 即将删掉，调试用
                          logger.info('line=%s' % line)
                          raise
                  file_list.append({'name': name, 'attr': attrs[1:], 'size': size, 'last_modify_time': last_modify_time})
              elif attrs[0] == 'l':  # link
                  if is_busybox:
                      name = _handle_name(items[8])
                      last_modify_time = items[7]
                      link = items[10]
                  elif is_toybox:
                      name = items[7]
                      last_modify_time = time.strptime('%s %s:00' % (items[5], items[6]), "%Y-%m-%d %X")
                      link = items[9]
                  else:
                      name = items[5]
                      last_modify_time = time.strptime('%s %s:00' % (items[3], items[4]), "%Y-%m-%d %X")
                      link = items[7]
                  file_list.append({'name': name, 'attr': attrs[1:], 'link': link, 'last_modify_time': last_modify_time})
                  
          return dir_list, file_list
      
      def get_sdcard_path(self):
          '''获取sdcard路径
          '''
          path = '/sdcard'
          while True:
              dir_list, file_list = self.list_dir(path)
              if len(dir_list) == 0 and len(file_list) == 1 and 'link' in file_list[0]:
                  # another link
                  path = file_list[0]['link']
              else:
                  break
          return path
      
      def get_file_info(self, file_path):
          '''获取文件信息
          '''
          return self.list_dir(file_path)[1][0]
  
      def copy_file(self, src_path, dst_path):
          '''在手机上拷贝文件
          '''
          if not hasattr(self, '_has_cp'):
              self._has_cp = 'not found' not in self.run_shell_cmd('cp')
          if self._has_cp:  # 不是所有的ROM都有cp命令
              self.run_shell_cmd('cp %s %s' % (src_path, dst_path), self.is_rooted())
          else:
              self.run_shell_cmd('cat %s > %s' % (src_path, dst_path), self.is_rooted(), timeout=30)  # 部分手机上发现此方法耗时较多
  
      def delete_file(self, file_path):
          '''删除手机上文件
          '''
          if '*' in file_path:
              # 使用通配符时不能使用引号
              self.run_shell_cmd('rm -f %s' % file_path, self.is_rooted())
          else:
              file_path = file_path.replace('"', r'\"')
              self.run_shell_cmd('rm -f "%s"' % file_path, self.is_rooted())
      
      def delete_folder(self, folder_path):
          '''删除手机上的目录
          '''
          folder_path = folder_path.replace('"', r'\"')
          self.run_shell_cmd('rm -R "%s"' % folder_path, self.is_rooted())
      
      def run_as_by_app(self, package_name, cmdline, **kwargs):
          '''在app中执行命令
          '''        
          cmd_res_path = '/sdcard/qt4a_cmd_res.txt'
          self.delete_file(cmd_res_path)
          timeout = 30
          if kwargs.has_key('timeout'):
              timeout = kwargs['timeout']
          try:
              self.start_activity('%s/com.test.androidspy.inject.CmdExecuteActivity' % package_name, extra={'cmdline':cmdline, 'timeout':timeout}, wait=False)
          except Exception as e:
              if 'com.test.androidspy.inject.CmdExecuteActivity} does not exist' in e.args[0]:
                  raise RuntimeError('该命令需要重打包apk才能执行，请重打包apk并安装生成的apk包后重试')
              raise
          cmd_argv_list = cmdline.split()
          if len(cmd_argv_list) > 1 and cmd_argv_list[0] == 'pm' and cmd_argv_list[1] == 'clear':
              logger.info('run cmd:%s,return Success' % cmdline)
              time.sleep(2)
              return 'Success'
              
          time0 = time.time()
          while time.time() - time0 < timeout:
              try:
                  self.list_dir(cmd_res_path)
                  result = self.run_shell_cmd("cat %s" % cmd_res_path)
                  return result
              except RuntimeError, e:
                  logger.info('run_as_by_app exception:%s' % e); 
                  time.sleep(1) 
          raise TimeoutError("run_as_by_app timeout:%d" % timeout)
          
      def run_as(self, package_name, cmdline, **kwargs):
          '''以package_name权限执行命令
          '''
          if self.is_rooted():
              if self._need_quote: cmdline = '"%s"' % cmdline
              cmdline = 'su %s %s' % (package_name, cmdline)
              return self.run_shell_cmd(cmdline, False, **kwargs)
          
          if ':' in package_name: package_name = package_name.split(':')[0]  # 允许传入进程名
          if '&&' in cmdline:
              cmndline = 'run-as %s sh -c "%s"' % (package_name, cmdline)
          else:
              cmndline = 'run-as %s %s' % (package_name, cmdline)
          result = self.run_shell_cmd(cmndline, **kwargs)
          run_as_succ = False
          if 'is unknown' in result:
              logger.info('Package %s not installed' % package_name)
          elif 'not debuggable' in result:
              logger.info('Package %s is not debuggable' % package_name)
          elif 'Could not set capabilities: Operation not permitted' in result:
              logger.info('Samsung device has bug with run-as command')
          elif 'run-as: exec failed for' in result:
              raise RuntimeError(result)
          else:
              run_as_succ = True
          if not run_as_succ:
              try:
                  result = self.run_as_by_app(package_name, cmdline, **kwargs)
              except RuntimeError:
                  logger.exception('run %s as %s by app failed' % (cmdline, package_name))
                  raise PermissionError('run %s as %s failed' % (cmdline, package_name))
          return result
      
      def is_app_process64(self, process):
          '''是否是64位应用进程
          
          :param process: 进程名或进程ID
          :tytpe process: string/int
          '''
          process_name = ''
          if isinstance(process, (str, unicode)) and not process.isdigit():
              process_name = process
              pid = self.get_pid(process)
          else:
              pid = int(process)
          if pid <= 0: raise ValueError('process %s not exist' % process)
          if self.is_rooted():
              return 'app_process64' in self.run_shell_cmd('ls -l /proc/%d/exe' % pid, True)
          elif process_name:
              return 'app_process64' in self.run_as(process_name, 'ls -l /proc/%d/exe' % pid)
          else:
              raise ValueError('Non root device must pass process name')
  
      def _list_process(self):
          '''获取进程列表
          '''
          cmdline = 'ps'
          if self.get_sdk_version() >= 26:
              cmdline += ' -A'
          result = self.run_shell_cmd(cmdline).strip()
          lines = result.split('\n')
          busybox = False
          if lines[0].startswith('PID'): busybox = True
  
          result_list = []
          for i in range(1, len(lines)):
              lines[i] = lines[i].strip()
              if not lines[i]: continue
              items = lines[i].split()
              if not busybox:
                  if len(items) < 9:
                      err_msg = "ps命令返回格式错误：\n%s" % lines[i]
                      if len(items) == 8:
                          result_list.append({'pid': int(items[1]), 'ppid': int(items[2]), 'proc_name': items[7]})
                      else:
                          raise RuntimeError(err_msg)
                  else:
                      proc_name = items[8]
                      if len(proc_name) <= 1 and len(items) > 9: proc_name = items[9]
                      result_list.append({'pid': int(items[1]), 'ppid': int(items[2]), 'proc_name': proc_name})
              else:
                  idx = 4
                  cmd = items[idx]
                  if len(cmd) == 1:
                      # 有时候发现此处会有“N”
                      idx += 1
                      cmd = items[idx]
                  idx += 1
                  if cmd[0] == '{' and cmd[-1] == '}': cmd = items[idx]
                  ppid = 0
                  if items[1].isdigit(): ppid = int(items[1])  # 有些版本中没有ppid
                  result_list.append({'pid': int(items[0]), 'ppid': ppid, 'proc_name': cmd})
          return result_list
      
      def list_process(self):
          '''获取进程列表
          '''
          for _ in range(3):
              try:
                  return self._list_process()
              except RuntimeError, e:
                  logger.warn('%s' % e)
          else:
              raise RuntimeError('获取进程列表失败')
          
      def get_pid(self, proc_name):
          '''获取进程ID
          '''
          process_list = self.list_process()
          for process in process_list:
              if process['proc_name'] == proc_name:
                  return process['pid']
          return 0
      
      def get_process_status(self, pid):
          '''获取进程状态信息
          '''
          ret = self.run_shell_cmd('cat /proc/%d/status' % pid, True)
          result = {}
          for line in ret.split('\n'):
              if not line: continue
              if not ':' in line:
                  logger.warn('get_process_status line error: %r' % line)
                  continue
              key, value = line.split(':')
              result[key] = value.strip()
          return result
      
      def get_process_user(self, pid):
          '''get procees user name
          
          :param pid: process id
          :type  pid: int
          '''
          uid = -1
          cmdline = 'cat /proc/%d/status' % pid
          result = self.run_shell_cmd(cmdline).strip()
          for line in result.split('\n'):
              line = line.strip()
              if line.startswith('Uid:'):
                  uid = int(line.split()[1])
                  break
          if uid < 0: raise RuntimeError('get uid of process %d failed' % pid)
          if uid < 10000: return uid
          cmdline = 'cat /proc/%d/cmdline' % pid
          result = self.run_shell_cmd(cmdline).strip().split('\x00')[0]
          if ':' in result: result = result.split(':')[0]
          return result
          
      def kill_process(self, proc_name_or_pid):
          '''杀进程
          '''
          kill_list = []
          package_name = None
          process_list = self.list_process()
          for process in process_list:
              if isinstance(proc_name_or_pid, (str, unicode)) and proc_name_or_pid in process['proc_name']:
                  if process['proc_name'] == proc_name_or_pid:
                      # 保证主进程首先被杀
                      kill_list.insert(0, process['pid'])
                  else:
                      kill_list.append(process['pid'])
              elif process['pid'] == proc_name_or_pid:
                  kill_list.append(process['pid'])
  
          if not kill_list: return None  # 没有找到对应的进程
          if package_name == None and not self.is_rooted(): package_name = self.get_process_user(kill_list[0])
          for i, pid in enumerate(kill_list):
              kill_list[i] = 'kill -9 %d' % pid
          cmd_line = ' && '.join(kill_list)
  
          if package_name == 2000:
              # shell process
              result = self.run_shell_cmd(cmd_line)
          elif self.is_rooted():
              result = self.run_shell_cmd(cmd_line, True)
          elif isinstance(package_name, (str, unicode)):
              # package
              result = self.run_as(package_name, cmd_line)
          else:
              raise PermissionError('can\'t kill uid=%s process in non-root device' % package_name)
          
          if 'Operation not permitted' in result:
              raise PermissionError('run %s failed: %s' % (cmd_line, result))
          return True
  
      def get_device_imei(self):
          '''获取手机串号
          '''
          result = self.run_shell_cmd('dumpsys iphonesubinfo', self.is_rooted())
          for line in result.split('\n'):
              if line.find('Device ID') >= 0:
                  return line.split('=')[1].strip()
          raise RuntimeError('获取imei号失败：%r' % result)
  
      def get_cpu_total_time(self):
          cpu_time = 0
          result = self.run_shell_cmd('cat /proc/stat')
          result = result.split('\n')[0]
          for item in result.split(' '):
              item = item.strip()
              if not item: continue
              if item == 'cpu': continue
              cpu_time += int(item)
          return cpu_time
  
      def get_process_cpu_time(self, pid):
          result = self.run_shell_cmd('cat /proc/%d/stat' % pid)
          result = result.split(' ')
          utime = int(result[13])
          stime = int(result[14])
          cutime = int(result[15])
          cstime = int(result[16])
          return utime + stime + cutime + cstime
  
      def get_thread_cpu_time(self, pid, tid):
          result = self.run_shell_cmd('cat /proc/%d/task/%d/stat' % (pid, tid))
          result = result.split(' ')
          utime = int(result[13])
          stime = int(result[14])
          cutime = int(result[15])
          cstime = int(result[16])
          return utime + stime + cutime + cstime
  
      def get_process_cpu(self, proc_name, interval=0.1):
          '''获取进程中每个线程的CPU占用率
          '''
          pid = self.get_pid(proc_name)
          # print pid'
          if not pid: return None
          total_cpu1 = self.get_cpu_total_time()
          process_cpu1 = self.get_process_cpu_time(pid)
          thread_cpu1 = self.get_thread_cpu_time(pid, pid)
          time.sleep(interval)
          total_cpu2 = self.get_cpu_total_time()
          process_cpu2 = self.get_process_cpu_time(pid)
          thread_cpu2 = self.get_thread_cpu_time(pid, pid)
          total_cpu = total_cpu2 - total_cpu1
          process_cpu = process_cpu2 - process_cpu1
          thread_cpu = thread_cpu2 - thread_cpu1
          return process_cpu * 100 / total_cpu, thread_cpu * 100 / total_cpu
  
      @staticmethod
      def list_device():
          '''获取设备列表
          '''
          return LocalADBBackend.list_device()
  
      @staticmethod
      def is_local_device(device_id):
          '''是否是本地设备
          '''
          pattern = re.compile(r'([\w|\-|\.]+):(.+)')
          mat = pattern.match(device_id)
          if not mat or (mat.group(2).isdigit() and int(mat.group(2)) > 1024 and int(mat.group(2)) < 65536):
              return True
          else:
              return False
  
      @staticmethod
      def open_device(name_or_backend=None):
          '''打开设备
          '''
          if isinstance(name_or_backend, str):
              adb_backend = LocalADBBackend.open_device(name_or_backend)
          else:
              adb_backend = name_or_backend
          
          adb = ADB(adb_backend)
  
          if adb.is_rooted() and adb.is_selinux_opened():
              if not adb.close_selinux():
                  logger.warn('Close selinux failed')
                  # raise RuntimeError('关闭selinux失败，确认手机是否完美Root')
          return adb
  
      @staticmethod
      def connect_device(name):
          '''使用TCP连接设备
          '''
          proc = subprocess.Popen([adb_path, 'connect', name], stdout=subprocess.PIPE)
          result = proc.stdout.read()
          if result.find('unable to connect to') >= 0:
              print >> sys.stderr, result
              return False
          return True
  
      def get_cpu_time(self):
          '''获取手机全局总时间片和空闲时间片
          '''
          import re
          cpu_time = 0
          result = self.run_shell_cmd('cat /proc/stat')
          result = result.split('\n')[0]
          result, num = re.subn(r'\s+', ' ', result)  # 将字符串中多个相连的空白字符合并成一个空白字符
          results = result.split(' ')
          if len(results) < 5 :
              logger.warn('无法取得CPU时间片统计，请确保手机正常链接，并已启动！')
              return 0, 0
          idle_time = int(results[4])
          for item in results:
              item = item.strip()
              if not item: continue
              if item == 'cpu': continue
              cpu_time += int(item)
          return cpu_time, idle_time
  
      def get_cpu_usage(self, interval=0.5):
          '''获取手机全局CPU使用率
          '''
          total_time1, idle_time1 = self.get_cpu_time()
          time.sleep(interval)
          total_time2, idle_time2 = self.get_cpu_time()
          total_time = total_time2 - total_time1
          idle_time = idle_time2 - idle_time1
          if total_time == 0 :
              return -1
          return (total_time - idle_time) * 100 / total_time
  
      @static_result
      def is_art(self):
          '''是否是art虚拟机
          '''
          ret = self.get_property('persist.sys.dalvik.vm.lib')
          if not ret: ret = self.get_property('persist.sys.dalvik.vm.lib.2')
          return 'libart.so' in ret
  
      def dump_stack(self, pid_or_procname):
          '''获取进程调用堆栈
          '''
          if isinstance(pid_or_procname, (str, unicode)):
              pid = self.get_pid(pid_or_procname)
          else:
              pid = pid_or_procname
          anr_dir = '/data/anr'
          try:
              self.list_dir(anr_dir)
          except RuntimeError:
              self.mkdir(anr_dir)
          self.chmod(anr_dir, 777)
          cmd = 'kill -3 %d' % pid
          self.run_shell_cmd(cmd, True)
          return self.run_shell_cmd('cat %s/traces.txt' % anr_dir, True)
      
      def get_state(self):
          '''获取设备状态
          '''
          return self.run_adb_cmd('get-state')
  
  if __name__ == '__main__':
      pass
      