def stop_device_server(self, device_name: str):
        """
        停止设备的minitouch服务(不清除端口)

        @param {str} device_name - 设备名
        """
        # 为后台线程送停止的参数
        self.devices[device_name]['stop_var'][0] = True

        # 获取minitouch进程id
        _pid = None
        _connection: MiniTouchConnection = self.devices[device_name].pop(
            'connection', None)
        if _connection is not None:
            _pid = _connection.pid
            # 停止socket连接
            _connection.disconnect()
        else:
            # 通过adb shell查找进程id
            try:
                _cmd_info = AdbTools.adb_run(
                    self.adb_name,
                    device_name,
                    'shell ps | %s %s' %
                    ('findstr' if sys.platform == 'win32' else 'grep',
                     self.devices[device_name]['minitouch_file']),
                    shell_encoding=self.shell_encoding)
                # shell        31976 31974 2169772   6832 __skb_wait_for_more_packets 0 S minicap
                _pid = _cmd_info[0][_cmd_info[0].find(' '):].strip().split(
                    ' ')[0]
            except:
                self.logger.warning(
                    'get device[%s] minitouch pid by adb error: %s' %
                    (device_name, traceback.format_exc()))

        # 结束进程
        if _pid is not None:
            AdbTools.adb_run(self.adb_name,
                             device_name,
                             'shell kill %s' % _pid,
                             shell_encoding=self.shell_encoding,
                             ignore_error=True)

        # 等待线程结束
        while self.devices[device_name]['server_thread'] is not None:
            time.sleep(0.1)
    def get_screen_wm(cls, device_name: str, adb_name: str = 'adb',
                      shell_encoding: str = None) -> tuple:
        """
        获取设备屏幕大小

        @param {str} device_name - 设备名
        @param {str} adb_name='adb' - adb命令名
        @param {str} shell_encoding=None - shell的编码

        @returns {tuple} - 设备屏幕大小,width, height
        """
        _cmd_info = AdbTools.adb_run(
            adb_name, device_name, 'shell wm size',
            shell_encoding=shell_encoding
        )
        _size_str = _cmd_info[0].split(':')[1].strip().split('x')
        return int(_size_str[0]), int(_size_str[1])
    def _get_connected_devices(self) -> dict:
        """
        获取已经连接上且未展示的设备清单字典

        @returns {dict} - 设备清单字典
            {
                '设备名称': {
                    ...
                },
                ...
            }
        """
        _adb = self.config_services.para['adb']
        _devices = dict()
        _full_device_list = list()
        _cmd_info = AdbTools.adb_run(_adb, '', 'devices -l')

        for _str in _cmd_info:
            if not _str.startswith('List of'):
                # 找到设备连接号
                _device_name = (_str[0:_str.find(' ')]).strip()

                if _device_name == '':
                    # 非法数据或已连接,不处理
                    continue

                # 登记完整清单
                _full_device_list.append(_device_name)

                if _device_name in self.devices.keys(
                ) and self.devices[_device_name]['status'] != 'error':
                    # 设备已存在,不处理
                    continue

                # 获取设备信息
                _devices[_device_name] = self._get_device_info(_device_name)

        # 检查是否有设备掉线
        _current_devices = list(self.devices.keys())
        for _device_name in _current_devices:
            if _device_name not in _full_device_list:
                # 设备已掉线
                self.remove_unconnect_device(_device_name)

        # 返回结果字典
        return _devices
    def init_device_server(cls,
                           device_name: str,
                           shared_path: str,
                           adb_name: str = 'adb',
                           shell_encoding: str = None):
        """
        初始化设备上的服务文件

        @param {str} device_name - 设备名
        @param {str} shared_path - 预编译的minitouch文件所在路径,该路径下文件应按该目录结构存放:
            shared_path/stf_libs/{abi}/minitouch
            其中{abi}为设备的cpu架构, 例如 arm64-v8a
        @param {str} adb_name='adb' - adb命令名
        @param {str} shell_encoding=None - shell的编码
        """
        # 检查sdk版本和cpu架构
        _cpu = AdbTools.adb_run(adb_name,
                                device_name,
                                'shell getprop ro.product.cpu.abi',
                                shell_encoding=shell_encoding)[0]
        _sdk = AdbTools.adb_run(adb_name,
                                device_name,
                                'shell getprop ro.build.version.sdk',
                                shell_encoding=shell_encoding)[0]

        # sdk小于16的版本要用nopie版本
        _minitouch_file = 'minitouch' if int(_sdk) >= 16 else 'minitouch-nopie'
        if not AdbTools.adb_file_exists(adb_name,
                                        device_name,
                                        '/data/local/tmp/%s' % _minitouch_file,
                                        shell_encoding=shell_encoding):
            # 推送相应文件到设备
            AdbTools.adb_run(
                adb_name,
                device_name,
                'push %s /data/local/tmp' %
                os.path.join(shared_path, 'stf_libs', _cpu, _minitouch_file),
                shell_encoding=shell_encoding)
            # 授权
            AdbTools.adb_run(adb_name,
                             device_name,
                             'shell chmod 777 /data/local/tmp/%s' %
                             _minitouch_file,
                             shell_encoding=shell_encoding)
    def start_device_server(self, device_name: str, show_size: tuple = None, canvas_size: tuple = None,
                            orientation: int = 0, quality: int = 80) -> int:
        """
        启动设备的minicap服务
        注:必须要先通过 init_device_server 对设备进行初始化处理

        @param {str} device_name - 设备号
        @param {tuple} show_size=None - 显示大小,(宽, 高),默认原生大小
        @param {tuple} canvas_size=None - 画布大小,(宽, 高),默认原生大小
        @param {int} orientation=0 - 手机旋转角度,支持 0 | 90 | 180 | 270
        @param {int} quality=80 - 视频质量,可设置0-100,降低视频质量可提高性能

        @param {dict} - 返回设备信息
        """
        # 获取该设备使用的端口
        if device_name in self.devices.keys():
            # 未回收端口,继续复用
            _port = self.devices[device_name]['port']
        else:
            # 新申请端口
            _port = self.minicap_ports.pop(0)

        # 处理显示大小
        _real_size = self.get_screen_wm(
            device_name, adb_name=self.adb_name, shell_encoding=self.shell_encoding
        )
        _show_size = show_size
        if show_size is None:
            _show_size = copy.deepcopy(_real_size)
        else:
            _show_size = self.get_show_size(
                _real_size, _show_size
            )

        # 处理画布大小
        _canvas_size = canvas_size
        if canvas_size is None:
            _canvas_size = copy.deepcopy(_real_size)
        else:
            _canvas_size = self.get_show_size(
                _real_size, _canvas_size
            )

        # 添加到设备清单
        self.devices[device_name] = {
            'port': _port,
            'stop_var': [False, ],  # 控制停止的参数
            'real_size': _real_size,
            'orientation': orientation,
            'quality': quality,
            'show_size': _show_size,
            'canvas_size': _canvas_size,
        }

        # 执行处理
        try:
            # 获取设备minicap版本
            _sdk = AdbTools.adb_run(
                self.adb_name, device_name, 'shell getprop ro.build.version.sdk',
                shell_encoding=self.shell_encoding
            )[0]
            self.devices[device_name]['minicap_file'] = 'minicap' if int(
                _sdk) >= 16 else 'minicap-nopie'

            # 映射端口,映射前需要回收端口
            AdbTools.adb_run(
                self.adb_name, device_name, 'forward --remove tcp:%d' % _port,
                shell_encoding=self.shell_encoding, ignore_error=True
            )

            AdbTools.adb_run(
                self.adb_name, device_name, 'forward tcp:%d localabstract:minicap' % _port,
                shell_encoding=self.shell_encoding
            )

            # 启动服务
            self._start_device_server(device_name)

            # 等待设备启动并检查状态
            time.sleep(self.start_wait_time)
            if self.devices[device_name].get('server_thread', None) is None:
                raise RuntimeError('server thread exited!')
        except:
            # 出现异常代表失败,将端口放回列表
            self.stop_device_server(device_name)  # 先尝试关闭服务
            self.devices.pop(device_name, None)
            self.minicap_ports.append(_port)
            self.logger.error(
                'start devices[%s] minicap server error: %s' % (
                    device_name, traceback.format_exc()
                )
            )
            raise

        return self.devices[device_name]
    def start_device_server(self, device_name: str) -> int:
        """
        启动设备的minitouch服务
        注:必须要先通过 init_device_server 对设备进行初始化处理

        @param {str} device_name - 设备号

        @param {int} - minitouch映射的监听端口
        """
        # 获取该设备使用的端口
        if device_name in self.devices.keys():
            # 未回收端口,继续复用
            _port = self.devices[device_name]['port']
        else:
            # 新申请端口
            _port = self.minitouch_ports.pop(0)

        # 添加到设备清单
        self.devices[device_name] = {
            'port': _port,
            'stop_var': [
                False,
            ],  # 控制停止的参数
        }

        # 执行处理
        try:
            # 获取设备minitouch版本
            _sdk = AdbTools.adb_run(self.adb_name,
                                    device_name,
                                    'shell getprop ro.build.version.sdk',
                                    shell_encoding=self.shell_encoding)[0]
            self.devices[device_name]['minitouch_file'] = 'minitouch' if int(
                _sdk) >= 16 else 'minitouch-nopie'

            # 映射端口,映射前需要回收端口
            AdbTools.adb_run(self.adb_name,
                             device_name,
                             'forward --remove tcp:%d' % _port,
                             shell_encoding=self.shell_encoding,
                             ignore_error=True)

            AdbTools.adb_run(self.adb_name,
                             device_name,
                             'forward tcp:%d localabstract:minitouch' % _port,
                             shell_encoding=self.shell_encoding)

            # 启动服务
            self._start_device_server(device_name)

            # 等待设备启动并检查状态
            time.sleep(self.start_wait_time)
            if self.devices[device_name].get('server_thread', None) is None:
                raise RuntimeError('server thread exited!')

            # 连接设备
            self.devices[device_name]['connection'] = MiniTouchConnection(
                '127.0.0.1',
                self.devices[device_name]['port'],
                buffer_size=self.buffer_size,
                encoding=self.encoding,
                logger=self.logger)
        except:
            # 出现异常代表失败,将端口放回列表
            self.stop_device_server(device_name)  # 先尝试关闭服务
            self.devices.pop(device_name, None)
            self.minitouch_ports.append(_port)
            self.logger.error('start devices[%s] minitouch server error: %s' %
                              (device_name, traceback.format_exc()))
            raise

        return _port
    def _get_device_info(self, device_name: str) -> dict:
        """
        获取设备信息

        @param {str} device_name - 设备名称(注意需已连接adb)

        @returns {dict} - 设备信息字典, 注意如果没有连接上也能正常返回,只是相应key值不存在
            {
                'device_name': '',  # 设备名称
                'platform_name': 'Android',  # 手机平台名
                'platform_version': '',  # 手机平台版本
                'wlan_ip': '',  # 手机连接的 wifi ip
                'brand': '',  # 手机的品牌
                'model': ''  # 手机的产品名称
            }
        """
        _device_info = {'device_name': device_name, 'platform_name': 'Android'}
        _adb = self.config_services.para['adb']
        _shell_encoding = self.config_services.para['shell_encoding']
        # 获取版本
        _device_info['platform_version'] = ''
        try:
            _cmd_info = AdbTools.adb_run(
                _adb,
                device_name,
                'shell getprop ro.build.version.release',
                shell_encoding=_shell_encoding)
            _device_info['platform_version'] = _cmd_info[0]
        except:
            pass

        # 获取手机厂商品牌
        _device_info['brand'] = ''
        try:
            _cmd_info = AdbTools.adb_run(_adb,
                                         device_name,
                                         'shell getprop ro.product.brand',
                                         shell_encoding=_shell_encoding)
            _device_info['brand'] = _cmd_info[0]
        except:
            pass

        # 获取手机产品名称
        _device_info['model'] = ''
        try:
            _cmd_info = AdbTools.adb_run(_adb,
                                         device_name,
                                         'shell getprop ro.product.model',
                                         shell_encoding=_shell_encoding)
            _device_info['model'] = _cmd_info[0]
        except:
            pass

        # 获取WIFI地址
        _device_info['wlan_ip'] = ''
        try:
            _cmd_info = AdbTools.adb_run(_adb,
                                         device_name,
                                         'shell ifconfig wlan0',
                                         shell_encoding=_shell_encoding)
            for _line in _cmd_info:
                _line = _line.strip()
                if _line.startswith('inet addr:'):
                    _temp_len = len('inet addr:')
                    _device_info['wlan_ip'] = _line[_temp_len:_line.
                                                    find(' ', _temp_len)]
                    break
        except:
            pass

        return _device_info