├── version.py
├── res
├── uispy.conf
├── file.png
├── folder.png
├── startup.png
├── qt4i_osx.icns
└── qt4i_win.ico
├── requirements.txt
├── .gitignore
├── .pydevproject
├── .project
├── util
├── __init__.py
└── logger.py
├── ui
├── __init__.py
├── app.py
├── sandboxframe.py
└── mainframe.py
├── rpc
├── __init__.py
├── driver.py
└── client.py
├── settings.py
├── README.md
├── uispy.spec
├── LICENSE.TXT
└── CONTRIBUTING.md
/version.py:
--------------------------------------------------------------------------------
1 | VERSION = "2.1.11"
2 |
--------------------------------------------------------------------------------
/res/uispy.conf:
--------------------------------------------------------------------------------
1 | [uispy]
2 | bundle_id = com.apple.Maps
3 |
4 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | wxPython==4.0.0b1
2 | PyInstaller==3.3.1
3 | qt4i
--------------------------------------------------------------------------------
/res/file.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qtacore/iOSUISpy/HEAD/res/file.png
--------------------------------------------------------------------------------
/res/folder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qtacore/iOSUISpy/HEAD/res/folder.png
--------------------------------------------------------------------------------
/res/startup.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qtacore/iOSUISpy/HEAD/res/startup.png
--------------------------------------------------------------------------------
/res/qt4i_osx.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qtacore/iOSUISpy/HEAD/res/qt4i_osx.icns
--------------------------------------------------------------------------------
/res/qt4i_win.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qtacore/iOSUISpy/HEAD/res/qt4i_win.ico
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # python编译文件
2 | *.pyc
3 | # eclipse生成的备份文件
4 | *.bak
5 | # 打包自动生成的目录
6 | build/
7 | dist/
8 | # 日志文件
9 | *.log
10 | # 调试目录
11 | debug/
12 | # Mac系统自动生成的文件
13 | *.DS_Store
14 | # virtual python环境,用于安装pyinstaller工具
15 | venv/
16 |
--------------------------------------------------------------------------------
/.pydevproject:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | /${PROJECT_DIR_NAME}
5 |
6 | python 2.7
7 | Default
8 |
9 |
--------------------------------------------------------------------------------
/.project:
--------------------------------------------------------------------------------
1 |
2 |
3 | UISpy
4 |
5 |
6 |
7 |
8 |
9 | org.python.pydev.PyDevBuilder
10 |
11 |
12 |
13 |
14 |
15 | org.python.pydev.pythonNature
16 |
17 |
18 |
--------------------------------------------------------------------------------
/util/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-#
2 | # Tencent is pleased to support the open source community by making QTA available.
3 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved.
4 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this
5 | # file except in compliance with the License. You may obtain a copy of the License at
6 | #
7 | # https://opensource.org/licenses/BSD-3-Clause
8 | #
9 | # Unless required by applicable law or agreed to in writing, software distributed
10 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS
11 | # OF ANY KIND, either express or implied. See the License for the specific language
12 | # governing permissions and limitations under the License.
13 | #
14 |
--------------------------------------------------------------------------------
/ui/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | #
3 | # Tencent is pleased to support the open source community by making QTA available.
4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved.
5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this
6 | # file except in compliance with the License. You may obtain a copy of the License at
7 | #
8 | # https://opensource.org/licenses/BSD-3-Clause
9 | #
10 | # Unless required by applicable law or agreed to in writing, software distributed
11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS
12 | # OF ANY KIND, either express or implied. See the License for the specific language
13 | # governing permissions and limitations under the License.
14 | #
15 | '''UISpy
16 | '''
--------------------------------------------------------------------------------
/rpc/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Tencent is pleased to support the open source community by making QTA available.
4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved.
5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this
6 | # file except in compliance with the License. You may obtain a copy of the License at
7 | #
8 | # https://opensource.org/licenses/BSD-3-Clause
9 | #
10 | # Unless required by applicable law or agreed to in writing, software distributed
11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS
12 | # OF ANY KIND, either express or implied. See the License for the specific language
13 | # governing permissions and limitations under the License.
14 | #
15 | '''QT4i rpc相关接口的封装
16 | '''
--------------------------------------------------------------------------------
/settings.py:
--------------------------------------------------------------------------------
1 | # -*- coding:utf-8 -*-
2 | #
3 | # Tencent is pleased to support the open source community by making QTA available.
4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved.
5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this
6 | # file except in compliance with the License. You may obtain a copy of the License at
7 | #
8 | # https://opensource.org/licenses/BSD-3-Clause
9 | #
10 | # Unless required by applicable law or agreed to in writing, software distributed
11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS
12 | # OF ANY KIND, either express or implied. See the License for the specific language
13 | # governing permissions and limitations under the License.
14 | #
15 | '''配置参数
16 | '''
17 |
18 | import sys
19 |
20 | if getattr(sys, 'frozen', False):
21 | # we are running in a bundle
22 | RESOURCE_PATH = sys._MEIPASS # @UndefinedVariable
23 | else:
24 | # we are running in a normal Python environment
25 | RESOURCE_PATH = '../res'
26 |
27 |
--------------------------------------------------------------------------------
/ui/app.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Tencent is pleased to support the open source community by making QTA available.
4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved.
5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this
6 | # file except in compliance with the License. You may obtain a copy of the License at
7 | #
8 | # https://opensource.org/licenses/BSD-3-Clause
9 | #
10 | # Unless required by applicable law or agreed to in writing, software distributed
11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS
12 | # OF ANY KIND, either express or implied. See the License for the specific language
13 | # governing permissions and limitations under the License.
14 | #
15 | '''UISpy App启动入口
16 | '''
17 |
18 |
19 |
20 | import wx
21 |
22 | from mainframe import MainFrame
23 | from util.logger import Log
24 |
25 |
26 | class UISpyApp(wx.App):
27 |
28 | def OnInit(self):
29 | self.main = MainFrame()
30 | self.main.Center()
31 | self.main.Show()
32 | self.SetTopWindow(self.main)
33 | return True
34 |
35 |
36 | if __name__ == '__main__':
37 | try:
38 | Log.i('main','UISpy started...')
39 | app = UISpyApp()
40 | app.MainLoop()
41 | except:
42 | import traceback
43 | message = traceback.format_exc()
44 | Log.e('main', message)
45 | dialog = wx.MessageDialog(None, message, u"错误", wx.OK|wx.ICON_ERROR)
46 | dialog.ShowModal()
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # iOSUISpy
2 |
3 | iOSUISpy is a UI Tool for [QT4i](https://github.com/tencent/qt4i) to inspect QPath of iOS controls for iOS App.
4 |
5 | ## Features
6 | * Inspect QPath or id of controls for any app in iOS device
7 | * Support sandbox viewing of iOS app
8 | * Support iOS app installation and uninstallation
9 | * Supoort iOS device remote control
10 |
11 |
12 | ## Get Started
13 |
14 | ### How to install iOSUISpy
15 |
16 | - Download [iOSUISpy Release Version](https://github.com/qtacore/iOSUISpy/releases) with the suffix ".dmg".
17 | - Move UISpy app to " /Applications" directory.
18 |
19 | ### How to use iOSUISpy
20 |
21 | - inspect QPath of iOS controls for iOS App
22 | - Open iOSUISpy by click icon of iOSUISpy.
23 | - Click "连接" button and wait until device is connected.
24 | - Select ios device and select ios app by bundle id.
25 | - Click "启动App" button and wait until sceenshot and ui tree of app appear.
26 | - Operate app to specified page and click "获取控件" button, and repeat this step for inspecting QPath.
27 |
28 |
29 | ### How to debug iOSUISpy project
30 |
31 | #### Debug with iOSUISpy source
32 |
33 | - Make sure that you are in Python 2.7 environment on MacOS system
34 |
35 | - Enter iOSUISpy project directory and install requirements library in terminal:
36 | ```shell
37 | $ pip install -i requirements.txt --user
38 | ```
39 |
40 | - Select 'ui/app.py' file and run
41 |
42 | #### Build iOSUISpy Release version
43 |
44 | Run command line in Terminal in iOSUISpy project root directory, and the executable app will appear in the directory of "dist".
45 | ```shell
46 | $ pyinstaller --windowed --clean --noconfirm --onedir uispy.spec
47 | ```
48 |
--------------------------------------------------------------------------------
/uispy.spec:
--------------------------------------------------------------------------------
1 | # -*- mode: python -*-
2 |
3 | import os
4 | import sys
5 |
6 |
7 | block_cipher = None
8 |
9 |
10 | project_path = os.getcwd()
11 | pkgs = [project_path]
12 | for filename in os.listdir(project_path):
13 | filepath = os.path.join(project_path, filename)
14 | if os.path.isdir(filepath):
15 | if '__init__.py' in os.listdir(filepath):
16 | pkgs.append(filepath)
17 |
18 |
19 | a = Analysis(['ui/app.py'],
20 | pathex=pkgs,
21 | binaries=[],
22 | datas=[('res','.')],
23 | hiddenimports=[],
24 | hookspath=[],
25 | runtime_hooks=[],
26 | excludes=[],
27 | win_no_prefer_redirects=False,
28 | win_private_assemblies=False,
29 | cipher=block_cipher)
30 | pyz = PYZ(a.pure, a.zipped_data,
31 | cipher=block_cipher)
32 |
33 | import version
34 | VERSION = version.VERSION
35 |
36 | if sys.platform == 'win32':
37 | exe = EXE(pyz,
38 | a.scripts,
39 | a.binaries,
40 | a.zipfiles,
41 | a.datas,
42 | name='UISpy',
43 | icon='res\\qt4i_win.ico',
44 | debug=False,
45 | strip=False,
46 | upx=True,
47 | console=False )
48 | else:
49 | exe = EXE(pyz,
50 | a.scripts,
51 | exclude_binaries=True,
52 | name='UISpy',
53 | debug=False,
54 | strip=False,
55 | upx=True,
56 | console=True )
57 |
58 | coll = COLLECT(exe,
59 | a.binaries,
60 | a.zipfiles,
61 | a.datas,
62 | strip=False,
63 | upx=True,
64 | name='UISpy')
65 |
66 | app = BUNDLE(coll,
67 | name='UISpy.app',
68 | icon='res/qt4i_osx.icns',
69 | info_plist={
70 | 'CFBundleName': 'UISpy',
71 | 'CFBundleDisplayName': 'UISpy',
72 | 'CFBundleGetInfoString': "QT4i UISpy",
73 | 'CFBundleIdentifier': "com.tencent.ios.uispy",
74 | 'CFBundleVersion': VERSION,
75 | 'CFBundleShortVersionString': VERSION,
76 | 'NSHumanReadableCopyright': u"Copyright(c)2010-2017 Tencent All Rights Reserved."
77 | },
78 | )
79 |
--------------------------------------------------------------------------------
/LICENSE.TXT:
--------------------------------------------------------------------------------
1 | Tencent is pleased to support the open source community by making QTA available.
2 | Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved.
3 | If you have downloaded a copy of the QTA binary from Tencent, please note that the QTA binary is licensed under the BSD 3-Clause License.
4 | If you have downloaded a copy of the QTA source code from Tencent, please note that QTAsource code is licensed under the BSD 3-Clause License, except for the third-party components listed below which are subject to different license terms. Your integration ofQTA into your own projects may require compliance with the BSD 3-ClauseLicense, as well as the other licenses applicable to the third-party components included within QTA.
5 | A copy of theBSD 3-Clause License is included in this file.
6 |
7 | Terms of the BSD 3-Clause License:
8 | --------------------------------------------------------------------
9 |
10 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
11 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
12 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
13 | 3. Neither the name of [copyright holder] nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
15 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to iOSUISpy
2 | Welcome to [report Issues](https://github.com/qtacore/iOSUISpy/issues) or [pull requests](https://github.com/qtacore/iOSUISpy/pulls). It's recommended to read the following Contributing Guild first to make contributing earlier.
3 |
4 | ## issues
5 | We use Git Issues to track public bugs and feature requests.
6 |
7 | ### Search Known Issues First
8 | Please search the exist issues to see if any similar issue or feature request has already been filed. You shold try to make sure your issue doesn't already exist.
9 |
10 | ### Reporting New Issues
11 | If you open an issue, the more information the better. Such as detailed description, screenshot or video of your problem, logcat or code blocks for your crash.
12 |
13 | ## Pull Requests
14 | We strongly welcome your pull request to make iOSUISpy better.
15 |
16 | ### Branch Management
17 | There are three main branch here:
18 |
19 | 1. `master` branch.
20 | 1. It is the latest (pre-)release branch. We use `master` for tag, with version number `1.1.0`, `1.2.0`, `1.3.0`...
21 | 2. **Don't submit any PR on `master` branch.**
22 | 2. `dev` branch.
23 | 1. It is our stable developing branch. After full testing, `dev` will publish to `master` branch for the next release.
24 | 2. **You are recommended to submit bugfix or feature PR on `dev` branch.**
25 | 3. `hotfix` branch.
26 | 1. It is the latest tag version for hot fix. If we accept your pull request, we may just tag with version number `1.1.1`, `1.2.3`.
27 | 2. **Only submit urgent PR on `hotfix` branch for next specific release.**
28 |
29 | Normal bugfix or feature request should submit on `dev` branch. After full testing, we will merge them on `master` branch for the next release.
30 |
31 | If you have some urgent bugfix on a published version, but the `master` branch have already far away with the latest tag version, you can submit a PR on hotfix. And it will be cherry picked to `dev` branch if it is possible.
32 |
33 | ```
34 | master
35 | ↑
36 | dev <--- hotfix PR
37 | ↑
38 | feature/bugfix PR
39 | ```
40 |
41 | ### Make Pull Requests
42 | The code team will monitor all pull request, we run some code check and test on it. After all tests passing, we will accecpt this pr. But it won't merge to `master` branch at once, which have some delay.
43 |
44 | Before submitting a pull request, please make sure the following is done
45 |
46 | 1. Fork the repo and create your branch from `master` or `hotfix`.
47 | 2. Update code or documentation if you have changed APIs.
48 | 3. Add the copyright notice to the top of any new files you've added.
49 | 4. Make sure your code lints and checkstyles.
50 | 5. Test and test again your code.
51 | 6. Now, you can submit your pull request on `dev` or `hotfix` branch.
52 |
53 | ## Code Style Guide
54 | Use [Code Style](https://www.python.org/dev/peps/pep-0008/) for Python.
55 |
56 | * 4 spaces for indentation rather than tabs
57 |
58 | ## License
59 | By contributing to iOSUISpy, you agree that your contributions will be licensed
60 | under its [BSD LICENSE](https://github.com/qtacore/iOSUISpy/blob/master/LICENSE.TXT)
--------------------------------------------------------------------------------
/util/logger.py:
--------------------------------------------------------------------------------
1 | # -*- coding: UTF-8 -*-
2 | #
3 | # Tencent is pleased to support the open source community by making QTA available.
4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved.
5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this
6 | # file except in compliance with the License. You may obtain a copy of the License at
7 | #
8 | # https://opensource.org/licenses/BSD-3-Clause
9 | #
10 | # Unless required by applicable law or agreed to in writing, software distributed
11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS
12 | # OF ANY KIND, either express or implied. See the License for the specific language
13 | # governing permissions and limitations under the License.
14 | #
15 |
16 | '''日志模块
17 | '''
18 |
19 |
20 | import os, sys
21 | import logging
22 | from logging.handlers import RotatingFileHandler
23 |
24 | class Log(object):
25 | '''
26 | '''
27 | logger = None
28 | log_name = 'UISpy'
29 |
30 | @staticmethod
31 | def gen_log_path():
32 | '''生成log存放路径
33 | '''
34 | if getattr(sys, 'frozen', False):
35 | dir_root = os.path.dirname(sys.executable)
36 | else:
37 | dir_root = os.path.dirname(os.path.abspath(__file__))
38 | log_name = '%s.log' % (Log.log_name)
39 | return os.path.join(dir_root, log_name)
40 |
41 | @staticmethod
42 | def get_logger():
43 | if Log.logger == None:
44 | Log.logger = logging.getLogger(Log.log_name)
45 | if len(Log.logger.handlers) == 0:
46 | Log.logger.setLevel(logging.DEBUG)
47 | Log.logger.addHandler(logging.StreamHandler(sys.stdout))
48 | fmt = logging.Formatter('%(asctime)s %(message)s') # %(filename)s %(funcName)s
49 | Log.logger.handlers[0].setFormatter(fmt)
50 |
51 | logger_path = Log.gen_log_path()
52 | file_handler = RotatingFileHandler(logger_path, mode='a', maxBytes=10*1024*1024,
53 | backupCount=2, encoding='utf-8', delay=0)
54 | fmt = logging.Formatter('%(asctime)s %(levelname)s %(thread)d %(message)s') # %(filename)s %(funcName)s
55 | file_handler.setFormatter(fmt)
56 | Log.logger.addHandler(file_handler)
57 | return Log.logger
58 |
59 | @staticmethod
60 | def call(func, tag, *args):
61 | '''
62 | '''
63 | msg = '[%s] %s' % (tag, ' '.join([arg.encode('utf8') if isinstance(arg, unicode) else arg for arg in args]))
64 | func = getattr(Log.get_logger(), func)
65 | return func(msg)
66 |
67 | @staticmethod
68 | def d(tag, *args):
69 | return Log.call('debug', tag, *args)
70 |
71 | @staticmethod
72 | def i(tag, *args):
73 | return Log.call('info', tag, *args)
74 |
75 | @staticmethod
76 | def w(tag, *args):
77 | return Log.call('warn', tag, *args)
78 |
79 | @staticmethod
80 | def e(tag, *args):
81 | return Log.call('error', tag, *args)
82 |
83 | @staticmethod
84 | def ex(tag, *args):
85 | return Log.call('exception', tag, *args)
86 |
87 |
--------------------------------------------------------------------------------
/ui/sandboxframe.py:
--------------------------------------------------------------------------------
1 | # -*- coding:utf-8 -*-
2 | #
3 | # Tencent is pleased to support the open source community by making QTA available.
4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved.
5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this
6 | # file except in compliance with the License. You may obtain a copy of the License at
7 | #
8 | # https://opensource.org/licenses/BSD-3-Clause
9 | #
10 | # Unless required by applicable law or agreed to in writing, software distributed
11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS
12 | # OF ANY KIND, either express or implied. See the License for the specific language
13 | # governing permissions and limitations under the License.
14 | #
15 | '''
16 | sandbox界面
17 | '''
18 | import wx
19 | import os
20 | import sys
21 | import base64
22 | import traceback
23 |
24 | from util.logger import Log
25 | from settings import RESOURCE_PATH
26 |
27 |
28 |
29 | SANDBOX_FILE_READ_FORM = ('.log', '.txt', '.plist', 'json', '.Indexed', '.conf', '.array', '.data', 'config')
30 |
31 | class TreeFrame(wx.Frame):
32 |
33 | def __init__(self, main_frame, root_path, title, device_driver, bundle_id):
34 | self._root_path = root_path
35 | self._main_frame = main_frame
36 | self._device_driver = device_driver
37 | self._bundle_id = bundle_id
38 | self._init_frame(title)
39 |
40 | def _init_frame(self, title):
41 | wx.Frame.__init__(self, None, -1, title, size=(950, 620), style=wx.DEFAULT_FRAME_STYLE & ~wx.MAXIMIZE_BOX & ~wx.RESIZE_BORDER)
42 | self.Bind(wx.EVT_CLOSE, self.on_close)
43 |
44 | self.directory_tree = {}
45 | self.image_list = wx.ImageList(14, 14)
46 | self.image_list.Add(wx.Image(os.path.join(RESOURCE_PATH, 'folder.png'), wx.BITMAP_TYPE_PNG).Scale(14,14).ConvertToBitmap())
47 | self.image_list.Add(wx.Image(os.path.join(RESOURCE_PATH, 'file.png'), wx.BITMAP_TYPE_PNG).Scale(14,14).ConvertToBitmap())
48 |
49 | self.treectrl = wx.TreeCtrl(self, wx.ID_ANY, size=(220, 600))
50 | self.treectrl.AssignImageList(self.image_list)
51 | self.treectrl.Bind(wx.EVT_TREE_SEL_CHANGED, self.on_tree_node_click)
52 | self.update_directory_tree()
53 |
54 | self.lbl = wx.TextCtrl(self, -1, pos=(220, 0), size=(730, 600), style=wx.TE_READONLY | wx.TE_MULTILINE)
55 |
56 | def update_tree_frame(self, title, device_driver, bundle_id):
57 | self.SetTitle(title)
58 | self.directory_tree = {}
59 | self._device_driver = device_driver
60 | self._bundle_id = bundle_id
61 | print 'bundle_id:%s' % self._bundle_id
62 | self.update_directory_tree()
63 |
64 | def update_directory_tree(self, bundle_id=None):
65 | self.treectrl.DeleteAllItems()
66 | if bundle_id:
67 | self._bundle_id = bundle_id
68 | self.treeroot = self.treectrl.AddRoot(self._bundle_id)
69 | self.treectrl.SetItemImage(self.treeroot, 0, which=wx.TreeItemIcon_Normal)
70 | # 添加子目录
71 | self.directory_tree[self.treeroot] = {'path':self._root_path, 'is_dir':True, 'expanded':True}
72 | self.add_item(self.treeroot, self._root_path)
73 |
74 | def on_tree_node_click(self, event):
75 | item_id = event.GetItem()
76 | path = self.directory_tree[item_id]['path']
77 | print path
78 | if self.directory_tree[item_id]['is_dir']:
79 | if not self.directory_tree[item_id]['expanded']:
80 | self.add_item(item_id, path)
81 | else:
82 | if os.path.splitext(path)[1] in SANDBOX_FILE_READ_FORM:
83 | try:
84 | content = self._device_driver.get_sandbox_file_content(self._bundle_id, path)
85 | self.lbl.SetValue(base64.b64decode(content))
86 | except:
87 | msg = traceback.format_exc() # 方式1
88 | print (msg)
89 | self.lbl.SetValue('该文件可能存在某些字符导致无法base64编码')
90 | else:
91 | self.lbl.SetValue('不支持该格式的文件显示')
92 |
93 | def add_item(self, root, path):
94 | self.directory_tree[root]['expanded'] = True
95 | try:
96 | file_list = self._device_driver.get_sandbox_path_files(self._bundle_id, path)
97 | for i in file_list:
98 | # 获得绝对路径
99 | tmpdir = os.path.join(path, i['path'])
100 | tmpdict = {}
101 | # 如果是路径的话 还需对该路径进行一次操作
102 | if i['is_dir']:
103 | child = self.treectrl.AppendItem(root, os.path.basename(i['path']))
104 | tmpdict['path'] = tmpdir
105 | tmpdict['is_dir'] = i['is_dir']
106 | tmpdict['expanded'] = False
107 | self.directory_tree[child] = tmpdict
108 | self.treectrl.SetItemImage(child, 0, which=wx.TreeItemIcon_Normal)
109 | tmpdict = {}
110 | # 如果是目录的话
111 | else:
112 | child = self.treectrl.AppendItem(root, os.path.basename(i['path']))
113 | tmpdict['path'] = tmpdir
114 | tmpdict['is_dir'] = i['is_dir']
115 | tmpdict['expanded'] = False
116 | self.directory_tree[child] = tmpdict
117 | self.treectrl.SetItemImage(child, 1, which=wx.TreeItemIcon_Normal)
118 | tmpdict = {}
119 | except:
120 | error = traceback.format_exc()
121 | Log.e('add_item', error)
122 | self._main_frame.create_tip_dialog(error.decode('utf-8'))
123 |
124 | def on_close(self, event):
125 | self._main_frame.on_close_treeFrame()
126 | self._device_driver.close_sandbox_client()
127 | event.Skip()
128 |
--------------------------------------------------------------------------------
/rpc/driver.py:
--------------------------------------------------------------------------------
1 | # -*- coding:utf-8 -*-
2 | #
3 | # Tencent is pleased to support the open source community by making QTA available.
4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved.
5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this
6 | # file except in compliance with the License. You may obtain a copy of the License at
7 | #
8 | # https://opensource.org/licenses/BSD-3-Clause
9 | #
10 | # Unless required by applicable law or agreed to in writing, software distributed
11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS
12 | # OF ANY KIND, either express or implied. See the License for the specific language
13 | # governing permissions and limitations under the License.
14 | #
15 | '''
16 | '''
17 |
18 | import base64
19 | import ConfigParser
20 | from functools import wraps
21 | import os
22 | import socket
23 | import sys
24 | import subprocess
25 | import threading
26 |
27 | from rpc.client import RPCClientProxy
28 | from settings import RESOURCE_PATH
29 |
30 | ENCODING = "utf-8"
31 |
32 |
33 | def sync(lockname):
34 | '''方法同步锁,保证driverserver的接口一次只访问一个
35 | '''
36 |
37 | def _synched(func):
38 | @wraps(func)
39 | def _synchronizer(self,*args, **kwargs):
40 | tlock = self.__getattribute__(lockname)
41 | tlock.acquire()
42 | try:
43 | return func(self, *args, **kwargs)
44 | finally:
45 | tlock.release()
46 | return _synchronizer
47 | return _synched
48 |
49 |
50 | class HostDriver(object):
51 | '''设备主机的Driver
52 | '''
53 |
54 |
55 | def __init__(self, host_ip, host_port):
56 | self._host_ip = host_ip
57 | self._host_url = 'http://%s:%s' % (host_ip, host_port)
58 | self._driver = RPCClientProxy('/'.join([self._host_url, 'host/']), allow_none=True, encoding=ENCODING)
59 | self._devices = None
60 | self._qt4i_manage = None
61 |
62 | @property
63 | def host_url(self):
64 | return self._host_url
65 |
66 | @property
67 | def devices(self):
68 | return self._devices
69 |
70 | @property
71 | def qt4i_manage(self):
72 | if sys.platform == 'darwin':
73 | config_parser = ConfigParser.ConfigParser()
74 | settings_file = os.path.join(RESOURCE_PATH, 'uispy.conf')
75 | config_parser.read(settings_file)
76 | try:
77 | self._qt4i_manage = config_parser.get('uispy', 'qt4i-manage')
78 | except ConfigParser.NoOptionError:
79 | self._qt4i_manage = os.path.expanduser("~/Library/Python/2.7/bin/qt4i-manage")
80 | config_parser.set("uispy", "qt4i-manage", self._qt4i_manage)
81 | with open(settings_file, 'w') as fd:
82 | config_parser.write(fd)
83 | return self._qt4i_manage
84 | else:
85 | raise Exception("Unsupported platform!")
86 |
87 | def connect_to_host(self, driver_type):
88 | socket.setdefaulttimeout(3)
89 | is_connected = False
90 | try:
91 | is_connected = self._driver.echo()
92 | except:
93 | is_connected = self.restart_host_driver(driver_type)
94 | finally:
95 | socket.setdefaulttimeout(None)
96 | return is_connected
97 |
98 | def restart_host_driver(self, driver_type):
99 | socket.setdefaulttimeout(3)
100 | is_connected = False
101 | if sys.platform == 'darwin' and self._host_ip == '127.0.0.1':
102 | try:
103 | if driver_type == 'instruments':
104 | subprocess.check_call('killall -9 instruments')
105 | xctestagent_path = os.path.join(os.path.expanduser('~'), 'XCTestAgent')
106 | if driver_type == 'xctest' and not os.path.exists(xctestagent_path):
107 | unzip_agent_cmd = '%s setup' % self.qt4i_manage
108 | subprocess.call(unzip_agent_cmd, shell=True)
109 | subprocess.call('%s restartdriver -t %s' % (self.qt4i_manage, driver_type), shell=True)
110 | self._driver = RPCClientProxy('/'.join([self.host_url, 'host/']), allow_none=True, encoding=ENCODING)
111 | is_connected = self._driver.echo()
112 | except:
113 | pass
114 | socket.setdefaulttimeout(None)
115 | return is_connected
116 |
117 | def list_devices(self):
118 | self._devices = self._driver.list_devices()
119 | return self._devices
120 |
121 | def start_simulator(self, udid):
122 | self._driver.start_simulator(udid)
123 |
124 |
125 | class DeviceDriver(object):
126 | '''iPhone真机或者模拟器的Driver
127 | '''
128 |
129 | def __init__(self, host_url, device_udid):
130 | self._driver = RPCClientProxy('/'.join([host_url, 'device', '%s/' % device_udid]), allow_none=True, encoding='utf-8')
131 | self.udid = device_udid
132 | self.devicelock = threading.RLock()
133 |
134 | @sync('devicelock')
135 | def start_app(self, bundle_id):
136 | try:
137 | self._driver.device.stop_app(bundle_id)
138 | except:
139 | pass
140 | return self._driver.device.start_app(bundle_id, None, None)
141 |
142 | @sync('devicelock')
143 | def take_screenshot(self):
144 | base64_img = self._driver.device.capture_screen()
145 | return base64.decodestring(base64_img)
146 |
147 | @sync('devicelock')
148 | def get_element_tree(self):
149 | return self._driver.device.get_element_tree()
150 |
151 | @sync('devicelock')
152 | def install_app(self, ipa_path):
153 | return self._driver.device.install(ipa_path)
154 |
155 | @sync('devicelock')
156 | def uninstall_app(self, bundle_id):
157 | return self._driver.device.uninstall(bundle_id)
158 |
159 | @sync('devicelock')
160 | def get_screen_orientation(self):
161 | return self._driver.device.get_screen_orientation()
162 |
163 | @sync('devicelock')
164 | def get_app_list(self, app_type="user"):
165 | '''获取设备上的app列表
166 | :param app_type: app的类型(user/system/all)
167 | :type app_type: str
168 | :returns: list 例如:[{'com.tencent.rdm': 'RDM'}]
169 | '''
170 | return self._driver.device.get_app_list(app_type)
171 |
172 | @sync('devicelock')
173 | def click(self, x, y, retry = 3):
174 | '''
175 | 基于屏幕的点击操作
176 | :param x: 横向坐标(从左向右,屏幕百分比)
177 | :type x: float
178 | :param y: 纵向坐标(从上向下,屏幕百分比)
179 | :type y: float
180 | :param retry 重试次数
181 | :type int
182 | '''
183 | self._driver.device.click(x, y)
184 |
185 | @sync('devicelock')
186 | def double_click(self, x, y):
187 | self._driver.device.double_click(x, y)
188 |
189 | @sync('devicelock')
190 | def long_click(self, x, y, duration=3):
191 | self._driver.device.long_click(x, y, duration)
192 |
193 | @sync('devicelock')
194 | def drag(self, x0, y0, x1, y1, duration=0, repeat=1, interval=0.5, velocity=1000, retry = 3):
195 | '''拖拽(全局操作)
196 | :param x0: 起始横向坐标(从左向右,屏幕百分比)
197 | :type x0: float
198 | :param y0: 起始纵向坐标(从上向下,屏幕百分比)
199 | :type y0: float
200 | :param x1: 终止横向坐标(从左向右,屏幕百分比)
201 | :type x1: float
202 | :param y1: 终止纵向坐标(从上向下,屏幕百分比)
203 | :type y1: float
204 | :param duration: 起始坐标按下的时间(秒)
205 | :type duration: float
206 | '''
207 | self._driver.device.drag(x0, y0, x1, y1, duration, repeat, interval, velocity)
208 |
209 | @sync('devicelock')
210 | def sendkeys(self, text):
211 | self._driver.device.send_keys(text)
212 |
213 | @sync('devicelock')
214 | def get_sandbox_path_files(self, bundle_id, file_path):
215 | '''返回真机或者模拟器的沙盒路径
216 |
217 | :param bundle_id: 应用的bundle_id
218 | :type bundle_id: str
219 | :param file_path: 沙盒目录
220 | :type file_path: str
221 | '''
222 | return self._driver.device.get_sandbox_path_files(bundle_id, file_path)
223 |
224 | @sync('devicelock')
225 | def is_sandbox_path_dir(self, bundle_id, file_path):
226 | '''判断一个sandbox路径是否是一个目录
227 |
228 | :param bundle_id: 应用的bundle_id
229 | :type bundle_id: str
230 | :param file_path: 沙盒目录
231 | :type file_path: str
232 | '''
233 | return self._driver.device.is_sandbox_path_dir(bundle_id, file_path)
234 |
235 | @sync('devicelock')
236 | def get_sandbox_file_content(self, bundle_id, file_path):
237 | '''获取sandbox中文本文件的内容
238 |
239 | :param bundle_id: 应用的bundle_id
240 | :type bundle_id: str
241 | :param file_path: 沙盒目录
242 | :type file_path: str
243 | '''
244 | return self._driver.device.get_sandbox_file_content(bundle_id, file_path)
245 |
246 | @sync('devicelock')
247 | def close_sandbox_client(self):
248 | '''销毁sandboxClient对象
249 | '''
250 | self._driver.device.close_sandbox_client()
251 |
252 | @sync('devicelock')
253 | def get_xcode_version(self):
254 | '''查询Xcode版本
255 |
256 | '''
257 | return self._driver.device.get_xcode_version()
258 |
259 | @sync('devicelock')
260 | def get_ios_version(self):
261 | '''查询ios版本
262 |
263 | '''
264 | return self._driver.device.get_ios_version()
265 |
--------------------------------------------------------------------------------
/rpc/client.py:
--------------------------------------------------------------------------------
1 | # -*- coding:utf-8 -*-
2 | #
3 | # Tencent is pleased to support the open source community by making QTA available.
4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved.
5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this
6 | # file except in compliance with the License. You may obtain a copy of the License at
7 | #
8 | # https://opensource.org/licenses/BSD-3-Clause
9 | #
10 | # Unless required by applicable law or agreed to in writing, software distributed
11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS
12 | # OF ANY KIND, either express or implied. See the License for the specific language
13 | # governing permissions and limitations under the License.
14 | #
15 | '''RPC Framework
16 | '''
17 |
18 | import json
19 | import random
20 | import string
21 | import SimpleXMLRPCServer
22 | import xmlrpclib
23 | try:
24 | import fcntl
25 | except ImportError:
26 | fcntl = None
27 |
28 | IDCHARS = string.ascii_lowercase+string.digits
29 |
30 | def random_id(length=8):
31 | return_id = ''
32 | for _ in range(length):
33 | return_id += random.choice(IDCHARS)
34 | return return_id
35 |
36 | class RPCClientProxy(object):
37 | '''RPC Client
38 | '''
39 |
40 | def __init__(self, uri, transport=None, encoding=None, verbose=0,
41 | allow_none=0, use_datetime=0, context=None):
42 | # establish a "logical" server connection
43 |
44 | if isinstance(uri, unicode):
45 | uri = uri.encode('ISO-8859-1')
46 |
47 | # get the url
48 | import urllib
49 | protocol, uri = urllib.splittype(uri)
50 | if protocol not in ("http", "https"):
51 | raise IOError, "unsupported JSON-RPC protocol"
52 | self.__host, self.__handler = urllib.splithost(uri)
53 | if not self.__handler:
54 | self.__handler = "/RPC2"
55 |
56 | if transport is None:
57 | if protocol == "https":
58 | transport = SafeTransport(use_datetime=use_datetime, context=context)
59 | else:
60 | transport = Transport(use_datetime=use_datetime)
61 | self.__transport = transport
62 |
63 | self.__encoding = encoding
64 | self.__verbose = verbose
65 | self.__allow_none = allow_none
66 |
67 | def __close(self):
68 | self.__transport.close()
69 |
70 | def __request(self, methodname, params):
71 | # call a method on the remote server
72 | request = {"jsonrpc": "2.0"}
73 | if len(params) > 0:
74 | request["params"] = params
75 | request["id"] = random_id()
76 | request["method"] = methodname
77 | request = json.dumps(request)
78 | response = self.__transport.request(
79 | self.__host,
80 | self.__handler,
81 | request,
82 | verbose=self.__verbose
83 | )
84 | response = json.loads(response)
85 | if not isinstance(response, dict):
86 | raise TypeError('Response is not dict')
87 | if 'error' in response.keys() and response['error'] is not None:
88 | raise DriverApiError(response['error']['message'])
89 | else:
90 | response = response['result'][0]
91 | if isinstance(response, dict):
92 | return self.encode_dict(response, "UTF-8")
93 | elif isinstance(response, list):
94 | return self.encode_list(response, "UTF-8")
95 | elif isinstance(response, unicode):
96 | return response.encode("UTF-8")
97 | return response
98 |
99 | def __repr__(self):
100 | return (
101 | "" %
102 | (self.__host, self.__handler)
103 | )
104 |
105 | __str__ = __repr__
106 |
107 | def __getattr__(self, name):
108 | # magic method dispatcher
109 | return xmlrpclib._Method(self.__request, name)
110 |
111 | # note: to call a remote object with an non-standard name, use
112 | # result getattr(server, "strange-python-name")(args)
113 |
114 | def __call__(self, attr):
115 | """A workaround to get special attributes on the ServerProxy
116 | without interfering with the magic __getattr__
117 | """
118 | if attr == "close":
119 | return self.__close
120 | elif attr == "transport":
121 | return self.__transport
122 | raise AttributeError("Attribute %r not found" % (attr,))
123 |
124 |
125 | def encode_dict(self, content, encoding="UTF-8"):
126 | '''将字典编码为指定形式
127 |
128 | :param content: 要编码内容
129 | :type content: dict
130 | :param encoding:编码类型
131 | :type encoding: str
132 | :returns: dict -- 编码后的字典
133 | '''
134 | for key in content:
135 | if isinstance(content[key], dict):
136 | content[key] = self.encode_dict(content[key], encoding)
137 | elif isinstance(content[key], unicode):
138 | content[key] = content[key].encode(encoding)
139 | elif isinstance(content[key], list):
140 | content[key] = self.encode_list(content[key], encoding)
141 | return content
142 |
143 | def encode_list(self, content, encoding="UTF-8"):
144 | '''将列表编码为指定形式
145 |
146 | :param content: 要编码内容
147 | :type content: list
148 | :param encoding:编码类型
149 | :type encoding: str
150 | :returns: list -- 编码后的列表
151 | '''
152 | for ind, item in enumerate(content):
153 | if isinstance(item, dict):
154 | content[ind] = self.encode_dict(item, encoding)
155 | elif isinstance(item, unicode):
156 | content[ind] = content[ind].encode(encoding)
157 | elif isinstance(item, list):
158 | content[ind] = self.encode_list(item, encoding)
159 | return content
160 |
161 |
162 | class DriverApiError(Exception):
163 | '''Driver API Error
164 | '''
165 |
166 | class Fault(object):
167 | '''JSON-RPC Error
168 | '''
169 |
170 | def __init__(self, code=-12306, message = None, rpcid=None):
171 | self.faultCode = code
172 | self.faultString = message
173 | self.rpcid = rpcid
174 | if not message:
175 | import traceback
176 | self.faultString = traceback.format_exc()
177 |
178 | def error(self):
179 | return {"code": self.faultCode, "message": self.faultString}
180 |
181 | def response(self):
182 | return json.dumps({"jsonrpc": "2.0", "error":self.error(), "id":self.rpcid})
183 |
184 | def __repr__(self):
185 | return '' % (self.faultCode, self.faultString)
186 |
187 |
188 | class TransportMixIn(object):
189 | '''XMLRPC Transport extended API
190 | '''
191 | user_agent = "jsonrpclib/0.1"
192 | _connection = (None, None)
193 | _extra_headers = []
194 |
195 | def send_content(self, connection, request_body):
196 | connection.putheader("Content-Type", "application/json-rpc")
197 | connection.putheader("Content-Length", str(len(request_body)))
198 | connection.endheaders()
199 | if request_body:
200 | connection.send(request_body)
201 |
202 | def getparser(self):
203 | target = JSONTarget()
204 | return JSONParser(target), target
205 |
206 |
207 | class JSONParser(object):
208 |
209 | def __init__(self, target):
210 | self.target = target
211 |
212 | def feed(self, data):
213 | self.target.feed(data)
214 |
215 | def close(self):
216 | pass
217 |
218 |
219 | class JSONTarget(object):
220 |
221 | def __init__(self):
222 | self.data = []
223 |
224 | def feed(self, data):
225 | self.data.append(data)
226 |
227 | def close(self):
228 | return ''.join(self.data)
229 |
230 |
231 | class Transport(TransportMixIn, xmlrpclib.Transport):
232 |
233 | def __init__(self, use_datetime):
234 | TransportMixIn.__init__(self)
235 | xmlrpclib.Transport.__init__(self, use_datetime)
236 |
237 |
238 | class SafeTransport(TransportMixIn, xmlrpclib.SafeTransport):
239 |
240 | def __init__(self, use_datetime, context):
241 | TransportMixIn.__init__(self)
242 | xmlrpclib.SafeTransport.__init__(self, use_datetime, context)
243 |
244 |
245 | class SimpleJSONRPCRequestHandler(SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):
246 | '''JSON-RPC请求处理器
247 | '''
248 | def is_rpc_path_valid(self):
249 | return True
250 |
251 | def do_POST(self):
252 | '''处理HTTP的POST请求
253 | '''
254 | if not self.is_rpc_path_valid():
255 | self.report_404()
256 | return
257 | try:
258 | max_chunk_size = 10*1024*1024
259 | size_remaining = int(self.headers["content-length"])
260 | L = []
261 | while size_remaining:
262 | chunk_size = min(size_remaining, max_chunk_size)
263 | L.append(self.rfile.read(chunk_size))
264 | size_remaining -= len(L[-1])
265 | data = ''.join(L)
266 | response = self.server._marshaled_dispatch(
267 | data, getattr(self, '_dispatch', None), self.path
268 | )
269 | self.send_response(200)
270 | except Exception:
271 | response = Fault().response()
272 | self.send_response(500, response)
273 | if response is None:
274 | response = ''
275 | self.send_header("Content-type", "application/json-rpc")
276 | if self.encode_threshold is not None:
277 | if len(response) > self.encode_threshold:
278 | q = self.accept_encodings().get("gzip", 0)
279 | if q:
280 | try:
281 | response = xmlrpclib.gzip_encode(response)
282 | self.send_header("Content-Encoding", "gzip")
283 | except NotImplementedError:
284 | pass
285 | self.send_header("Content-length", str(len(response)))
286 | self.end_headers()
287 | self.wfile.write(response)
288 |
--------------------------------------------------------------------------------
/ui/mainframe.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Tencent is pleased to support the open source community by making QTA available.
4 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved.
5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this
6 | # file except in compliance with the License. You may obtain a copy of the License at
7 | #
8 | # https://opensource.org/licenses/BSD-3-Clause
9 | #
10 | # Unless required by applicable law or agreed to in writing, software distributed
11 | # under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS
12 | # OF ANY KIND, either express or implied. See the License for the specific language
13 | # governing permissions and limitations under the License.
14 | #
15 | '''UISpy窗口控件
16 | '''
17 |
18 |
19 | import ConfigParser
20 | import os
21 | import StringIO
22 | import sys
23 | import threading
24 | import traceback
25 | import wx
26 | import subprocess
27 | import time
28 |
29 | from util.logger import Log
30 | from rpc.driver import HostDriver
31 | from rpc.driver import DeviceDriver
32 | from ui.sandboxframe import TreeFrame
33 | from version import VERSION
34 | from settings import RESOURCE_PATH
35 |
36 |
37 | DEFAULT_BUNDLE_ID = 'com.tencent.sng.test.gn'
38 | DEBUG_PATH = 'UISpy.app/Contents/MacOS/UISpy'
39 | TIMER_ID = 10010
40 |
41 |
42 | class EnumDriverType(object):
43 | '''定义iOS测试框架的类型
44 | '''
45 | XCTest, Instruments = ('xctest', 'instruments')
46 |
47 |
48 | class MainFrame(wx.Frame):
49 |
50 | def __init__(self):
51 | self._init_controls()
52 | self._driver_type = EnumDriverType.XCTest
53 | self._device = None
54 | self._host_driver = None
55 | self._device_driver = None
56 | self._element_tree = None
57 | self._focused_element = None
58 | self._app_started = False
59 | self._orientation = 1
60 | self._app_type = 'user'
61 | self._process_dlg_running = False
62 | self.treeframe = None
63 |
64 | def _init_controls(self):
65 | #设置MacOS X的偏移
66 | if sys.platform == 'darwin':
67 | osx_offset = 5
68 | else:
69 | osx_offset = 0
70 | uispy_stype = wx.DEFAULT_FRAME_STYLE & ~wx.MAXIMIZE_BOX & ~wx.RESIZE_BORDER #屏蔽掉最大化按钮和窗口缩放功能
71 | wx.Frame.__init__(self, None, wx.ID_ANY, "UISpy "+VERSION, size=(900, 750-osx_offset*6), style=uispy_stype)
72 |
73 | # 设置左上角图标(仅限windows系统)
74 | if sys.platform == 'win32':
75 | logo = wx.Icon(os.path.join(RESOURCE_PATH, 'qt4i_win.ico'), wx.BITMAP_TYPE_ICO)
76 | self.SetIcon(logo)
77 |
78 | # 设置菜单栏
79 | menu_bar = wx.MenuBar()
80 | #应用菜单
81 | app_menu = wx.Menu()
82 | install_menu = app_menu.Append(wx.ID_ANY, u"安装App", u"安装app到被测手机")
83 | uninstall_menu = app_menu.Append(wx.ID_ANY, u"卸载App", u"卸载被测手机上的app")
84 | sandbox_menu = app_menu.Append(wx.ID_ANY, u"浏览App沙盒", u"打开并查看设备的沙盒目录")
85 | app_menu.AppendSeparator()
86 | menu_bar.Append(app_menu, u'应用')
87 | #高级菜单
88 | advance_menu = wx.Menu()
89 | log_menu = advance_menu.Append(wx.ID_ANY, u"查看日志", u"打开日志文件夹")
90 | debug_menu = advance_menu.Append(wx.ID_ANY, u"Debug模式", u"使用Debug模式运行UISpy")
91 | setting_menu = advance_menu.Append(wx.ID_ANY, u"设置", u"环境参数设置")
92 | self.show_qpath_menu = advance_menu.Append(wx.ID_ANY, u'显示QPath', u"打开即可显示控件Qpath",kind=wx.ITEM_CHECK)
93 | self.remote_operator_menu = advance_menu.Append(wx.ID_ANY, u'远程控制', u"打开即可远程控制手机",kind=wx.ITEM_CHECK)
94 |
95 | advance_menu.AppendSeparator()
96 | menu_bar.Append(advance_menu, u'高级')
97 |
98 | self.Bind(wx.EVT_MENU, self.on_select_install_pkg, install_menu)
99 | self.Bind(wx.EVT_MENU, self.on_uninstall, uninstall_menu)
100 | self.Bind(wx.EVT_MENU, self.on_log, log_menu)
101 | self.Bind(wx.EVT_MENU, self.on_debug, debug_menu)
102 | self.Bind(wx.EVT_MENU, self.on_settings, setting_menu)
103 | self.Bind(wx.EVT_MENU, self.on_sandbox_view, sandbox_menu)
104 | self.SetMenuBar(menu_bar)
105 |
106 | # 设置主面板
107 | self.panel = wx.Panel(self, wx.ID_ANY)
108 | self.Bind(wx.EVT_CLOSE, self.on_close)
109 |
110 | # 第一行控件
111 | line1_y = 10
112 | wx.StaticText(self.panel, wx.ID_ANY, u"设备主机:", pos=(10,line1_y+osx_offset/2), size=wx.DefaultSize)
113 | self.tc_device_host_ip = wx.TextCtrl(self.panel, wx.ID_ANY, pos=(80, line1_y), size=(140, 24), style=wx.TE_LEFT)
114 | self.tc_device_host_ip.SetValue('127.0.0.1')
115 | self.tc_device_host_ip.SetToolTip(u"请输入设备主机或者设备DriverServer的IP")
116 | wx.StaticText(self.panel, wx.ID_ANY, u":", pos=(225, line1_y), size=wx.DefaultSize)
117 | self.tc_device_host_port = wx.TextCtrl(self.panel, wx.ID_ANY, pos=(235, line1_y), size=(60, 24), style=wx.TE_LEFT)
118 | self.tc_device_host_port.SetValue('12306')
119 | self.tc_device_host_port.SetToolTip(u"请输入设备主机或者设备DriverServer的端口号")
120 | self.btn_connect_device_host = wx.Button(self.panel, wx.ID_ANY, label=u'连接', pos=wx.Point(310, line1_y), size=wx.Size(50, 24), style=0)
121 | self.btn_connect_device_host.Bind(wx.EVT_BUTTON, self.on_connect_device_host)
122 |
123 | # 第二行控件
124 | line2_y = 45
125 | wx.StaticText(self.panel, wx.ID_ANY, u"设备:", pos=(10,line2_y+osx_offset/2), size=wx.DefaultSize)
126 | self.cb_devicelist = wx.ComboBox(self.panel, wx.ID_ANY, pos=(50, line2_y), size=(245+osx_offset/2, 24), style=wx.CB_READONLY)
127 | self.cb_devicelist.Bind(wx.EVT_COMBOBOX, self.on_select_device)
128 | self.btn_refresh = wx.Button(self.panel, wx.ID_ANY, u'刷新', pos=wx.Point(310, line2_y), size=wx.Size(50, 25), style=0)
129 | self.btn_refresh.Bind(wx.EVT_BUTTON, self.on_update_device_list)
130 | self.btn_refresh.Enable(False)
131 |
132 | # 第三行控件
133 | line3_y = 80
134 | wx.StaticText(self.panel, wx.ID_ANY, u"BundleID:", pos=(10,line3_y+osx_offset/2), size=wx.DefaultSize)
135 | self.tc_bundle_id = wx.ComboBox(self.panel, wx.ID_ANY, pos=(80-osx_offset, line3_y), size=(230+osx_offset*2, 24))
136 | self._config_file_path = os.path.join(RESOURCE_PATH, 'uispy.conf')
137 | if os.path.exists(self._config_file_path):
138 | config_parser = ConfigParser.ConfigParser()
139 | config_parser.read(self._config_file_path)
140 | try:
141 | bunlde_id = config_parser.get('uispy', 'bundle_id')
142 | except ConfigParser.NoOptionError:
143 | bunlde_id = 'com.tencent.sng.test.gn'
144 | else:
145 | with open(self._config_file_path, 'w+') as fd:
146 | config_parser = ConfigParser.ConfigParser()
147 | config_parser.add_section('uispy')
148 | config_parser.write(fd)
149 | bunlde_id = DEFAULT_BUNDLE_ID
150 | self.tc_bundle_id.SetValue(bunlde_id)
151 | self.tc_bundle_id.SetToolTip(u"请选择被测App的Bundle ID")
152 | self.tc_bundle_id.Bind(wx.EVT_COMBOBOX, self.on_select_bundle_id)
153 | self.btn_refresh_applist = wx.Button(self.panel, wx.ID_ANY, u'刷新', pos=wx.Point(320, line3_y), size=wx.Size(40, 25), style=0)
154 | self.btn_refresh_applist.Bind(wx.EVT_BUTTON, self.on_update_app_list)
155 | self.btn_refresh_applist.Enable(False)
156 | self.tc_bundle_all = wx.CheckBox(self.panel, label = 'All',pos = (370, line3_y+osx_offset/2))
157 | self.tc_bundle_all.Bind(wx.EVT_CHECKBOX, self.on_select_bundle_all)
158 | self.btn_start_app = wx.Button(self.panel, wx.ID_ANY, label=u'启动App', pos=wx.Point(420, line3_y), size=wx.Size(80, 24), style=0)
159 | self.btn_start_app.Bind(wx.EVT_BUTTON, self.on_start_app)
160 |
161 | # 第四行控件
162 | line4_y = 115
163 | wx.StaticText(self.panel, wx.ID_ANY, u"QPath:", pos=(10,line4_y+osx_offset/2), size=wx.DefaultSize)
164 | self.tc_qpath = wx.TextCtrl(self.panel, wx.ID_ANY, pos=(70-osx_offset, line4_y), size=(340, 24), style=wx.TE_LEFT)
165 | self.tc_qpath.SetValue('')
166 | self.btn_get_uitree = wx.Button(self.panel, wx.ID_ANY, label=u'获取控件', pos=wx.Point(420, line4_y), size=wx.Size(80, 24), style=0)
167 | self.btn_get_uitree.Bind(wx.EVT_BUTTON, self.on_get_uitree)
168 |
169 | #截屏区域的控件
170 | self.image_shown_width = 375
171 | self.image_shown_height = 667
172 | self.image_screenshot = wx.StaticBitmap(parent=self.panel, id=wx.ID_ANY, pos=(520, 5), size=(self.image_shown_width, self.image_shown_height))
173 | self.image_screenshot.Bind(wx.EVT_MOUSE_EVENTS, self.on_screenshot_mouse_event)
174 | self.image_screenshot.Bind(wx.EVT_SET_FOCUS, self.on_screenshot_focus)
175 | startup_image = wx.Image(os.path.join(RESOURCE_PATH, 'startup.png'), wx.BITMAP_TYPE_ANY, index=-1)
176 | startup_image = startup_image.Scale(self.image_shown_width, self.image_shown_height)
177 | self.image_screenshot.SetBitmap(wx.Bitmap(startup_image))
178 | self.mask_panel = CanvasPanel(self.panel, id=wx.ID_ANY, pos=(520, 5), size=(self.image_shown_width, self.image_shown_height))
179 | self.mask_panel.Bind(wx.EVT_MOUSE_EVENTS, self.on_screenshot_mouse_event)
180 | #文件拖拽
181 | self.drop_target = FileDropTarget(self, self.image_screenshot)
182 | self.image_screenshot.SetDropTarget(self.drop_target)
183 |
184 | #控件树
185 | line5_y = 150
186 | self.tc_uitree = wx.TreeCtrl(self.panel, wx.ID_ANY,pos=(10, line5_y), size=(500, 380))
187 | self.tc_uitree.Bind(wx.EVT_TREE_SEL_CHANGED, self.on_uitree_node_click)
188 | self.tc_uitree.Bind(wx.EVT_TREE_ITEM_RIGHT_CLICK, self.on_uitree_node_right_click)
189 |
190 | #控件详细属性
191 | line6_y = 525
192 | st_control_properties = wx.StaticBox(self.panel, wx.ID_ANY, u"控件属性", pos=(10, line6_y+osx_offset*2), size=(500, 140))
193 | wx.StaticText(st_control_properties, wx.ID_ANY, u"name", pos=(10, 30-osx_offset*2), size=wx.DefaultSize)
194 | self.tc_id = wx.TextCtrl(st_control_properties, wx.ID_ANY, pos=(55, 30-osx_offset*2), size=(135, 24), style=wx.TE_LEFT)
195 | wx.StaticText(st_control_properties, wx.ID_ANY, u"classname", pos=(200, 30-osx_offset*2), size=wx.DefaultSize)
196 | self.tc_classname = wx.TextCtrl(st_control_properties, wx.ID_ANY, pos=(280, 30-osx_offset*2), size=(160, 24), style=wx.TE_LEFT)
197 | wx.StaticText(st_control_properties, wx.ID_ANY, u"label", pos=(10, 65-osx_offset*2), size=wx.DefaultSize)
198 | self.tc_label = wx.TextCtrl(st_control_properties, wx.ID_ANY, pos=(55, 65-osx_offset*2), size=(135, 24), style=wx.TE_LEFT)
199 | wx.StaticText(st_control_properties, wx.ID_ANY, u"rect", pos=(200, 65-osx_offset*2), size=wx.DefaultSize)
200 | self.tc_rect = wx.TextCtrl(st_control_properties, wx.ID_ANY, pos=(250, 65-osx_offset*2), size=(190, 24), style=wx.TE_LEFT)
201 | wx.StaticText(st_control_properties, wx.ID_ANY, u"value", pos=(10, 100-osx_offset*2), size=wx.DefaultSize)
202 | self.tc_value = wx.TextCtrl(st_control_properties, wx.ID_ANY, pos=(55, 100-osx_offset*2), size=(135, 24), style=wx.TE_LEFT)
203 | wx.StaticText(st_control_properties, wx.ID_ANY, u"visible", pos=(200, 100-osx_offset*2), size=wx.DefaultSize)
204 | self.tc_visible = wx.TextCtrl(st_control_properties, wx.ID_ANY, pos=(250, 100-osx_offset*2), size=(50, 24), style=wx.TE_LEFT)
205 | wx.StaticText(st_control_properties, wx.ID_ANY, u"enable", pos=(310, 100-osx_offset*2), size=wx.DefaultSize)
206 | self.tc_enable = wx.TextCtrl(st_control_properties, wx.ID_ANY, pos=(370, 100-osx_offset*2), size=(50, 24), style=wx.TE_LEFT)
207 |
208 | #状态栏
209 | self.statusbar = self.CreateStatusBar()
210 | # 将状态栏分割为3个区域,比例为1:2:3
211 | self.statusbar.SetFieldsCount(3)
212 | self.statusbar.SetStatusWidths([-6, -6, -1])
213 |
214 | def on_close(self, event):
215 | if self.treeframe:
216 | self.treeframe.Destroy()
217 | config_parser = ConfigParser.ConfigParser()
218 | config_parser.read(self._config_file_path)
219 | config_parser.set("uispy", "bundle_id", self.tc_bundle_id.GetValue())
220 | with open(self._config_file_path, 'w') as fd:
221 | config_parser.write(fd)
222 | import atexit
223 | atexit._exithandlers = [] # 禁止退出时弹出错误框
224 | event.Skip()
225 |
226 | def create_tip_dialog(self, msg, title=u"错误"):
227 | dialog = wx.MessageDialog(self, msg, title, style=wx.OK)
228 | dialog.ShowModal()
229 | dialog.Destroy()
230 |
231 | def show_process_dialog(self, msg):
232 | progress_max = 100
233 | dialog = wx.ProgressDialog(u"提示", msg, progress_max, parent=self.panel, style=wx.PD_CAN_ABORT|wx.PD_APP_MODAL|wx.PD_AUTO_HIDE)
234 | count = 0
235 | while self._process_dlg_running and count < progress_max:
236 | count = (count + 1) % 100
237 | wx.MilliSleep(100)
238 | if not dialog.Update(count)[0]:
239 | break
240 | dialog.Destroy()
241 |
242 | def _run_in_main_thread(self, func, *args, **kwargs):
243 | wx.CallAfter(func, *args, **kwargs)
244 |
245 | def _run_in_work_thread(self, func, *args, **kwargs):
246 | t = threading.Thread(target=func, args=args, kwargs=kwargs)
247 | t.setDaemon(True)
248 | t.start()
249 |
250 | def _update_device_list(self):
251 | self._run_in_main_thread(self.statusbar.SetStatusText, u"正在获取设备列表......", 0)
252 | time.sleep(2)
253 | try:
254 | start_time = time.time()
255 | devices = self._host_driver.list_devices()
256 | end_time = time.time()
257 | print 'cost time:',(end_time - start_time)
258 | except:
259 | error = traceback.format_exc()
260 | Log.e('update_device_list', error)
261 | self._run_in_main_thread(self.statusbar.SetStatusText, u"更新设备列表失败", 0)
262 | self._run_in_main_thread(self.create_tip_dialog, error.decode('utf-8'))
263 | return
264 | self.cb_devicelist.Clear()
265 | for dev in devices:
266 | self.cb_devicelist.Append(dev['name'].decode('utf-8'), dev)
267 | self._device = devices[0]
268 | self._run_in_main_thread(self.btn_refresh.Enable)
269 | self._run_in_main_thread(self.btn_refresh_applist.Enable)
270 | self._run_in_main_thread(self.cb_devicelist.Select, 0)
271 | self._run_in_main_thread(self.statusbar.SetStatusText, u"更新设备列表完毕", 0)
272 | self._update_bundle_id_list()
273 |
274 | def _update_bundle_id_list(self):
275 | '''
276 | 在连接设备或者更换设备时,更新bundle_id列表
277 | '''
278 | print self._device
279 | self._device_driver = DeviceDriver(self._host_driver.host_url, self._device['udid'])
280 | original_bundle_id = self.tc_bundle_id.GetValue()
281 | self.tc_bundle_id.Clear()
282 | if self._device['simulator']:
283 | self._host_driver.start_simulator(self._device['udid'])
284 | self._app_list = self._device_driver.get_app_list(self._app_type)
285 | Log.i('app_list:', str(self._app_list))
286 | bundle_list = []
287 | remove_dev = None
288 | if self._app_list:
289 | for dev in self._app_list:
290 | bundle = dev.keys()[0]
291 | if bundle == 'com.apple.test.XCTestAgent-Runner':
292 | remove_dev = dev
293 | continue
294 | bundle_list.append(bundle)
295 | self.tc_bundle_id.Append(bundle.decode('utf-8'), dev)
296 | index = bundle_list.index(original_bundle_id) if original_bundle_id and original_bundle_id in bundle_list else 0
297 | self._run_in_main_thread(self.tc_bundle_id.Select, index)
298 | if remove_dev:
299 | self._app_list.remove(dev)
300 | if self.treeframe:
301 | self._run_in_main_thread(self.on_update_sandbox_view)
302 | else:
303 | self.create_tip_dialog(u'设备未启动或无可用APP')
304 | return
305 |
306 | def on_connect_device_host(self, event):
307 | '''连接设备主机
308 | '''
309 | self.statusbar.SetStatusText(u"正在连接设备主机......", 0)
310 | host_ip = self.tc_device_host_ip.GetValue()
311 | host_port = self.tc_device_host_port.GetValue()
312 | self._host_driver = HostDriver(host_ip, host_port)
313 | if not self._host_driver.connect_to_host(self._driver_type):
314 | self.statusbar.SetStatusText(u"连接设备主机异常!", 0)
315 | config_parser = ConfigParser.ConfigParser()
316 | config_parser.read(os.path.join(RESOURCE_PATH, 'uispy.conf'))
317 | qt4i_manage = config_parser.get('uispy', 'qt4i-manage')
318 | if not os.path.exists(qt4i_manage):
319 | self.create_tip_dialog(u"请检查qt4i是否安装或者qt4i-manage路径是否配置正确!\n"
320 | u"1.qt4i安装方法:pip install qt4i --user\n"
321 | u"2.qt4i-manage配置方法:高级->设置")
322 | else:
323 | self.create_tip_dialog(u"连接设备主机异常,请检查设备主机地址")
324 | return
325 | self._run_in_work_thread(self._update_device_list)
326 |
327 | def on_select_device(self, event):
328 | '''从设备列表中选择设备
329 | '''
330 | index = self.cb_devicelist.GetSelection()
331 | selected_device = self.cb_devicelist.GetClientData(index)
332 |
333 | if self._device != selected_device:
334 | self._device = selected_device
335 | print 'current_device:', self._device
336 | self._run_in_work_thread(self._update_bundle_id_list)
337 |
338 | def on_update_device_list(self, event):
339 | '''刷新设备列表
340 | '''
341 | self.statusbar.SetStatusText(u"正在更新设备列表......", 0)
342 | self._run_in_work_thread(self._update_device_list)
343 |
344 | def _update_screenshot(self, img_data):
345 | image = wx.Image(StringIO.StringIO(img_data))
346 | if self._orientation == 3 and image.GetWidth() > image.GetHeight():
347 | image = image.Rotate90()
348 | w = image.GetWidth()
349 | h = image.GetHeight()
350 | print 'Width:', w
351 | print 'Height:', h
352 | image = image.Scale(self.image_shown_width, self.image_shown_height)
353 | self.image_screenshot.SetBitmap(wx.Bitmap(image))
354 |
355 | def _add_child(self, parent, children, depth):
356 | for child in children:
357 | properties = dict.copy(child)
358 | del properties['children']
359 | new_item = self.tc_uitree.AppendItem(parent, child['classname'], data = properties)
360 | child['item_id'] = new_item
361 | child['depth'] = depth
362 | self._add_child(new_item, child['children'], depth+1)
363 |
364 | def _update_uitree(self):
365 | self.tc_uitree.DeleteAllItems()
366 | if 'UIATarget' == self._element_tree['classname']:
367 | self._element_tree = self._element_tree['children'][0]
368 | app = dict.copy(self._element_tree)
369 | del app['children']
370 |
371 | self._app_shown_width = app['rect']['size']['width']
372 | self._app_shown_height = app['rect']['size']['height']
373 |
374 | if self._scale_rate is None:
375 | self._scale_rate = ((self.image_shown_width * 1.0) / self._app_shown_width,
376 | (self.image_shown_height * 1.0) / self._app_shown_height)
377 | # root = self.tc_uitree.AddRoot(self._element_tree['classname'], data = wx.TreeItemData(app))
378 | root = self.tc_uitree.AddRoot(self._element_tree['classname'], data = app)
379 | self._element_tree['item_id'] = root
380 | self._element_tree['depth'] = -1
381 | self._root_item = root
382 | self._add_child(root, self._element_tree['children'], 0)
383 |
384 | def _start_app(self):
385 | bundle_id = self.tc_bundle_id.GetValue()
386 |
387 | while(True):
388 | if self._process_dlg_running:
389 | break
390 | dlg = self._dialog
391 | try:
392 | xcode_version = self._device_driver.get_xcode_version()
393 | ios_version = self._device_driver.get_ios_version()
394 | Log.i('xcode_version:%s ios_version:%s' % (xcode_version, ios_version))
395 | # 增加版本检测
396 | self._run_in_main_thread(dlg.on_update)
397 | self._app_started = self._device_driver.start_app(bundle_id)
398 | if self._app_started:
399 | self._run_in_main_thread(self.statusbar.SetStatusText, u"App启动成功", 0)
400 | self._run_in_main_thread(dlg.on_update_title_msg, '抓取App屏幕中......')
401 | img_data = self._device_driver.take_screenshot()
402 | self._orientation = self._device_driver.get_screen_orientation()
403 | self._run_in_main_thread(self._update_screenshot, img_data)
404 | self._run_in_main_thread(dlg.on_update_title_msg, '抓取App控件树中......')
405 | self._element_tree = self._device_driver.get_element_tree()
406 | self._run_in_main_thread(self._update_uitree)
407 | else:
408 | self._run_in_main_thread(self.statusbar.SetStatusText, u"App启动失败:", 0)
409 | except:
410 | error = traceback.format_exc()
411 | Log.e('start_app', error)
412 | self._run_in_main_thread(self.create_tip_dialog, error.decode('utf-8'))
413 | self._process_dlg_running = False
414 | self._run_in_main_thread(dlg.on_destory)
415 |
416 | def on_start_app(self, event):
417 | if self._host_driver is None:
418 | self.create_tip_dialog(u"未连接设备主机,请连接设备主机")
419 | return
420 | self._device_driver = DeviceDriver(self._host_driver.host_url, self._device['udid'])
421 | self.statusbar.SetStatusText(u"App正在启动......", 0)
422 | self._scale_rate = None
423 | self._run_in_work_thread(self._start_app)
424 | self.show_dialog('App启动中......')
425 |
426 | def show_dialog(self, msg):
427 | self._dialog = MyProgressDialog(msg, self.panel)
428 | self._process_dlg_running = True
429 |
430 | def on_get_uitree(self, event):
431 | img_data = self._device_driver.take_screenshot()
432 | self._orientation = self._device_driver.get_screen_orientation()
433 | print 'orientation: ', self._orientation
434 | self._update_screenshot(img_data)
435 | try:
436 | self._element_tree = self._device_driver.get_element_tree()
437 | except:
438 | self.create_tip_dialog(u'获取控件树失败:%s' % traceback.format_exc())
439 | return
440 | self._update_uitree()
441 | self.statusbar.SetStatusText(u"获取控件树成功", 0)
442 |
443 | def _check_pos_in_element(self, pos, element):
444 | rect = element['rect']
445 | if pos[0] >= rect['origin']['x'] and pos[0] <= rect['origin']['x'] + rect['size']['width'] \
446 | and pos[1] >= rect['origin']['y'] and pos[1] <= rect['origin']['y'] + rect['size']['height'] \
447 | and element['visible']:
448 | return True
449 | else:
450 | return False
451 |
452 | def _select_closest_element(self, pos, element1, element2):
453 | '''选择距离最近的控件,具体规则如下:
454 | 1、如果element1和element2的位置是包含关系(父子关系),则选择子节点
455 | 2、如果element1和element2的位置是部分重叠,则选择距离控件中心最近的控件
456 | '''
457 | rect1 = element1['rect']
458 | rect2 = element2['rect']
459 | if rect1['origin']['x'] >= rect2['origin']['x'] and \
460 | rect1['origin']['x'] + rect1['size']['width'] <= rect2['origin']['x'] + rect2['size']['width'] and \
461 | rect1['origin']['y'] >= rect2['origin']['y'] and \
462 | rect1['origin']['y'] + rect1['size']['height'] <= rect2['origin']['y'] + rect2['size']['height']:
463 | return element1
464 |
465 | if rect2['origin']['x'] >= rect1['origin']['x'] and \
466 | rect2['origin']['x'] + rect2['size']['width'] <= rect1['origin']['x'] + rect1['size']['width'] and \
467 | rect2['origin']['y'] >= rect1['origin']['y'] and \
468 | rect2['origin']['y'] + rect2['size']['height'] <= rect1['origin']['y'] + rect1['size']['height']:
469 | return element2
470 |
471 | center1 = (rect1['origin']['x'] + rect1['size']['width'] / 2.0, rect1['origin']['y'] + rect1['size']['height'] / 2.0)
472 | center2 = (rect2['origin']['x'] + rect2['size']['width'] / 2.0, rect2['origin']['y'] + rect2['size']['height'] / 2.0)
473 | distance1 = (pos[0] - center1[0]) ** 2 + (pos[1] - center1[1]) ** 2
474 | distance2 = (pos[0] - center2[0]) ** 2 + (pos[1] - center2[1]) ** 2
475 | if distance1 <= distance2:
476 | return element1
477 | else:
478 | return element2
479 |
480 | def _get_focused_element(self, pos, root):
481 | '''优先查找叶子节点的控件(深度遍历)
482 | '''
483 | for e in root['children']:
484 | self._get_focused_element(pos, e)
485 | if self._check_pos_in_element(pos, e):
486 | if self._focused_element is None:
487 | self._focused_element = e
488 | else:
489 | self._focused_element = self._select_closest_element(pos, self._focused_element, e)
490 |
491 | # if self._check_pos_in_element(pos, root):
492 | # if self._focused_element is None:
493 | # self._focused_element = root
494 | # else:
495 | # self._focused_element = self._select_closest_element(pos, self._focused_element, root)
496 |
497 | def _expand_uitree(self, item_id):
498 | '''展开控件树
499 | '''
500 | if item_id != self._root_item:
501 | parent = self.tc_uitree.GetItemParent(item_id)
502 | self._expand_uitree(parent)
503 | self.tc_uitree.Expand(item_id)
504 | else:
505 | self.tc_uitree.Expand(self._root_item)
506 |
507 | def _dfs_traverse(self, element_tree, element_list=None):
508 | if element_list is None:
509 | element_list = []
510 | element_list.append(element_tree)
511 | for e in element_tree['children']:
512 | self._dfs_traverse(e, element_list)
513 | return element_list
514 |
515 | def _recommend_qpath(self, element):
516 | '''推荐QPath,具体策略如下:
517 | 1、id唯一,则推荐id作为QPath
518 | 2、待补充
519 | '''
520 | self.tc_qpath.SetValue("")
521 | if element['name']:
522 | element_id = element['name']
523 | element_list = self._dfs_traverse(self._element_tree)
524 | count = 0
525 | for e in element_list:
526 | if e['name'] == element_id:
527 | count += 1
528 | if count == 1:
529 | self.tc_qpath.SetValue(element_id.decode('utf-8'))
530 | return
531 |
532 | if self.show_qpath_menu.IsChecked():
533 | if element['classname'] == 'Other' and not element['label'] and not element['name'] and not element['value']:
534 | return
535 |
536 | qpath = '/classname = \'' + element['classname'] + '\''
537 | if element['label']:
538 | qpath += ' & label = \'' +element['label'] + '\''
539 | elif element['name']:
540 | qpath += ' & name = \'' +element['name'] + '\''
541 | elif element['value']:
542 | qpath += ' & value = \'' +element['value'] + '\''
543 |
544 | qpath += ' & visible = %s & maxdepth = %s' % (str(element['visible']).lower(), str(element['depth']).lower())
545 | self.tc_qpath.SetValue(qpath.decode('utf-8'))
546 |
547 |
548 | def on_screenshot_focus(self, event):
549 | print 'screenshot focus'
550 | # self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
551 |
552 | def on_screenshot_single_click(self, event):
553 | self.timer.Stop()
554 | click_action = 'click'
555 | if not self._mouse_up:
556 | click_action = 'long_click'
557 | if not self._element_tree:
558 | Log.e('on_screenshot_single_click', 'failed to get element tree')
559 | return
560 | self._focused_element = None
561 | self._get_focused_element(self._single_click_position, self._element_tree)
562 | print 'focus_element:', self._focused_element ['rect']
563 | rect = (self._focused_element ['rect']['origin']['x'], self._focused_element ['rect']['origin']['y'], \
564 | self._focused_element ['rect']['size']['width'], self._focused_element ['rect']['size']['height'])
565 | #红色标记截图中鼠标位置的控件
566 | if self._orientation == 3:
567 | rect = (self.image_shown_width/self._scale_rate[0]-rect[1] - rect[3], rect[0], rect[3], rect[2])
568 | self.mask_panel.highlight_element(rect, self._scale_rate)
569 | #展开控件树
570 | self._expand_uitree(self._focused_element['item_id'])
571 | #选中对应的控件
572 | self.tc_uitree.SelectItem(self._focused_element['item_id'])
573 | self.tc_uitree.SetFocus()
574 | #推荐控件的QPath
575 | self._recommend_qpath(self._focused_element)
576 | #判断是否进行远程控制
577 | if self.remote_operator_menu.IsChecked() and self._device_driver:
578 | driver_method = getattr(self._device_driver, click_action)
579 | driver_method(self.x, self.y)
580 | time.sleep(1.5)
581 | self._run_in_work_thread(self.on_get_uitree,event)
582 |
583 | def on_screenshot_mouse_event(self, event):
584 | pos = event.GetPosition()
585 | if self._orientation == 3:
586 | pos = (pos[1], self.image_shown_width - pos[0])
587 |
588 | self.statusbar.SetStatusText(str(pos),2)
589 | if not self._app_started:
590 | self.statusbar.SetStatusText(u'App未启动',0)
591 | return
592 |
593 | if event.ButtonDown():
594 | self._mouse_up = False
595 | self._event_start = time.time()
596 | pos = (pos[0]/self._scale_rate[0], pos[1]/self._scale_rate[1])
597 | self.x = float(pos[0])/self._app_shown_width
598 | self.y = float(pos[1])/self._app_shown_height
599 | if not event.RightDown():
600 | self.timer = wx.Timer(self)
601 | self.timer.Start(200) # 0.2 seconds delay
602 | self.Bind(wx.EVT_TIMER, self.on_screenshot_single_click, self.timer)
603 |
604 | elif event.Dragging():
605 | self.timer.Stop()
606 |
607 | elif event.LeftDClick():
608 | self.timer.Stop()
609 |
610 | elif event.LeftUp():
611 | self._mouse_up = True
612 | self._event_interval = time.time() - self._event_start
613 | new_pos = (pos[0]/self._scale_rate[0], pos[1]/self._scale_rate[1])
614 | x1 = float(new_pos[0])/self._app_shown_width
615 | y1 = float(new_pos[1])/self._app_shown_height
616 | self._single_click_position = new_pos
617 | if abs(self.x - x1) > 0.02 or abs(self.y - y1) > 0.02:
618 | if self.remote_operator_menu.IsChecked() and self._device_driver:
619 | self._device_driver.drag(self.x, self.y, x1, y1, self._event_interval)
620 | time.sleep(1.5)
621 | self._run_in_work_thread(self.on_get_uitree,event)
622 |
623 | def on_uitree_node_click(self, event):
624 |
625 | item_id = event.GetItem()
626 |
627 | item_data = self.tc_uitree.GetItemData(item_id)
628 |
629 | item_data['depth'] = self.get_element_depth(item_id)
630 |
631 | self.tc_classname.SetValue(item_data['classname'])
632 | name = item_data['name'] if item_data['name'] else 'null'
633 | if isinstance(name, basestring):
634 | name = name.decode('utf-8')
635 | else:
636 | name = str(name)
637 | self.tc_id.SetValue(name)
638 | label = item_data['label'] if item_data['label'] else 'null'
639 | if isinstance(label, basestring):
640 | label = label.decode('utf-8')
641 | else:
642 | label = str(label)
643 | self.tc_label.SetValue(label)
644 | value = item_data['value'] if item_data['value'] else 'null'
645 | if isinstance(value, basestring):
646 | value = value.decode('utf-8')
647 | else:
648 | value = str(value)
649 | self.tc_value.SetValue(value)
650 | rect = (item_data['rect']['origin']['x'], item_data['rect']['origin']['y'], \
651 | item_data['rect']['size']['width'], item_data['rect']['size']['height'])
652 | self.tc_rect.SetValue(str(rect))
653 | self.tc_visible.SetValue('true' if item_data['visible'] else 'False')
654 | self.tc_enable.SetValue('true' if item_data['enabled'] else 'False')
655 |
656 | if self._orientation == 3:
657 | rect = (self.image_shown_width/self._scale_rate[0]-rect[1]-rect[3], rect[0], rect[3], rect[2])
658 | self.mask_panel.highlight_element(rect, self._scale_rate)
659 |
660 | self._recommend_qpath(item_data)
661 |
662 | def on_uitree_node_right_click(self, event):
663 | self.tc_uitree.PopupMenu(TreeNodePopupMenu(self, event.GetItem()), event.Point)
664 |
665 | def on_select_install_pkg(self, event):
666 | if self._device is None:
667 | self.create_tip_dialog(u'未选择设备,请连接设备主机并选择设备!')
668 | return
669 | dlg = wx.FileDialog(self, u"选择app的安装包", "", "",
670 | "app files (*.zip,*.ipa)|*.zip;*.ipa", wx.FD_OPEN | wx.FD_FILE_MUST_EXIST)
671 | if dlg.ShowModal() == wx.ID_CANCEL:
672 | return
673 | pkg_path = dlg.GetPath().encode('utf-8')
674 | dlg.Destroy()
675 | self._run_in_work_thread(self.on_install, pkg_path)
676 |
677 | def on_install(self, pkg_path):
678 |
679 | self._run_in_main_thread(self.show_dialog, 'APP正在安装中......')
680 | while(True):
681 | if self._process_dlg_running:
682 | break
683 | dlg = self._dialog
684 | self._run_in_main_thread(dlg.on_update)
685 |
686 | if not self._device_driver:
687 | self._device_driver = DeviceDriver(self._host_driver.host_url, self._device['udid'])
688 | try:
689 | result = self._device_driver.install_app(pkg_path)
690 | self._process_dlg_running = False
691 | self._run_in_main_thread(dlg.on_destory)
692 | if result:
693 | self._run_in_main_thread(wx.MessageBox, '安装成功', '信息提示', wx.OK|wx.ICON_INFORMATION)
694 | self._update_bundle_id_list()
695 | else:
696 | self._run_in_main_thread(wx.MessageBox, '安装失败', '信息提示', wx.OK|wx.ICON_INFORMATION)
697 | except:
698 | error = traceback.format_exc()
699 | Log.e('start_app', error)
700 | self.create_tip_dialog(error.decode('utf-8'))
701 |
702 | def on_uninstall(self, event):
703 | if self._device is None:
704 | self.create_tip_dialog(u'未选择设备,请连接设备主机并选择设备!')
705 | return
706 | if not self._device_driver:
707 | self._device_driver = DeviceDriver(self._host_driver.host_url, self._device['udid'])
708 | index = self.tc_bundle_id.GetSelection()
709 | bundle_id = self.tc_bundle_id.GetClientData(index).keys()[0]
710 | result = self._device_driver.uninstall_app(bundle_id)
711 |
712 | if result:
713 | uninstall_app = self.get_app(bundle_id)
714 | message_title = uninstall_app[bundle_id]+':'+bundle_id
715 | wx.MessageBox(u"卸载成功",message_title.decode('utf-8'))
716 | self._update_bundle_id_list()
717 | else:
718 | wx.MessageBox(u"卸载失败")
719 |
720 | def on_log(self, event):
721 | '''
722 | 打开日志所在文件夹
723 | '''
724 | log_path = Log.gen_log_path()
725 | if os.path.exists(log_path):
726 | if sys.platform == 'darwin':
727 | log_cmd = 'open %s' % log_path
728 | subprocess.call(log_cmd,shell=True)
729 | elif sys.platform == 'win32':
730 | os.system("explorer.exe %s" % log_path)
731 | else:
732 | self.create_tip_dialog(u'日志文件被移除')
733 | return
734 |
735 | def on_select_bundle_id(self, event):
736 | '''
737 | 当bundle_id切换选择时,更新沙盒目录
738 | '''
739 | if self.treeframe:
740 | self.treeframe.update_directory_tree(self.tc_bundle_id.GetValue())
741 |
742 | def on_update_app_list(self, event):
743 | '''
744 | 更新app列表
745 | '''
746 | if self._device:
747 | self._run_in_work_thread(self._update_bundle_id_list)
748 |
749 | def on_select_bundle_all(self, event):
750 | '''
751 | 根据按钮打开多有或者用户APP
752 | '''
753 | bundle_check = event.GetEventObject()
754 | if bundle_check.GetValue():
755 | self._app_type = 'all'
756 | else:
757 | self._app_type = 'user'
758 | if self._device:
759 | self._run_in_work_thread(self._update_bundle_id_list)
760 |
761 | def get_element_depth(self, item_id):
762 | '''
763 | 获取element的depth
764 | :param item_id element的属性
765 |
766 | return depth
767 | '''
768 | depth = 0
769 | while item_id != self._root_item:
770 | item_id = self.tc_uitree.GetItemParent(item_id)
771 | depth += 1
772 | return depth
773 |
774 | def get_app(self, bundle_id):
775 | '''
776 | 根据bundle_id从app_list中获取app信息
777 | '''
778 | for app in self._app_list:
779 | if app.has_key(bundle_id):
780 | return app
781 |
782 | def on_debug(self, event):
783 | '''
784 | '''
785 | current_path = os.getcwd()
786 | debug_path = os.path.join(current_path, DEBUG_PATH)
787 | if current_path.endswith('ui'):
788 | wx.MessageBox('本模式下不支持')
789 | else:
790 | self.Close(force=True)
791 | wx.Exit()
792 | cmd = 'open %s' % debug_path
793 | subprocess.call(cmd,shell=True)
794 |
795 | def on_settings(self, event):
796 | setting_dlg = Settings(self._config_file_path, None, title=u'环境参数设置')
797 | setting_dlg.ShowModal()
798 | setting_dlg.Destroy()
799 |
800 | def on_sandbox_view(self, event):
801 | '''
802 | 查看沙盒的目录结构
803 | '''
804 | if self._device is None:
805 | self.create_tip_dialog(u'未选择设备,请连接设备主机并选择设备!')
806 | return
807 | self.treeframe = TreeFrame(self, '/', '%s沙盒目录' % self._device['name'], self._device_driver, self.tc_bundle_id.GetValue())
808 | self.treeframe.Show(show=True)
809 |
810 | def on_update_sandbox_view(self):
811 | if self.treeframe:
812 | self.treeframe.update_tree_frame('%s沙盒目录' % self._device['name'], self._device_driver, self.tc_bundle_id.GetValue())
813 |
814 | def on_close_treeFrame(self):
815 | '''监控沙盒frame是否关闭
816 | '''
817 | self.treeframe = None
818 |
819 |
820 | class CanvasPanel(wx.Panel):
821 | '''绘图面板
822 | '''
823 | def __init__(self, *args, **kwargs):
824 | wx.Panel.__init__(self, *args, **kwargs)
825 | self.Bind(wx.EVT_PAINT, self.on_paint)
826 | self._draw_points = None
827 | self._last_draw_points = None
828 |
829 | def draw_rectangle(self, p1, p2):
830 | '''画长方形
831 | '''
832 | self._draw_points = (p1, p2)
833 | if self._last_draw_points and self._last_draw_points[0] == p1 and self._last_draw_points[1] == p2:
834 | pass
835 | else:
836 | self.Hide()
837 | self.Show()
838 |
839 | def highlight_element(self, rect, scale_rate):
840 | p1 = rect[0] * scale_rate[0], rect[1] * scale_rate[1]
841 | p2 = (rect[0] + rect[2]) * scale_rate[0], (rect[1] + rect[3]) * scale_rate[1]
842 | self.draw_rectangle(p1, p2)
843 |
844 | def on_paint(self, evt):
845 | if self._draw_points:
846 | left, top = self._draw_points[0]
847 | right, bottom = self._draw_points[1]
848 | dc = wx.PaintDC(self)
849 | dc.SetPen(wx.Pen('red', 2))
850 | dc.DrawLine(left, top, right, top)
851 | dc.DrawLine(right, top, right, bottom)
852 | dc.DrawLine(right, bottom, left, bottom)
853 | dc.DrawLine(left, bottom, left, top)
854 | self._last_draw_points = self._draw_points
855 | self._draw_points = None
856 |
857 |
858 | class TreeNodePopupMenu(wx.Menu):
859 | '''控件树节点的弹出菜单
860 | '''
861 |
862 | def __init__(self, parent, select_node, *args, **kwargs):
863 | super(TreeNodePopupMenu, self).__init__(*args, **kwargs)
864 | self._parent = parent
865 | self._select_node = select_node
866 |
867 | item1 = wx.MenuItem(self, wx.NewId(), u'打开WebInspector')
868 | self.AppendItem(item1)
869 | self.Bind(wx.EVT_MENU, self.on_open_web_inspector, item1)
870 |
871 | def on_open_web_inspector(self, event):
872 | if sys.platform == 'darwin':
873 | import webbrowser
874 | safari = webbrowser.get('safari')
875 | safari.open('', new=0, autoraise=True)
876 | else:
877 | self._parent.create_tip_dialog(u'WebInspector仅支持MacOS系统下Safari浏览器')
878 |
879 |
880 | class MyProgressDialog(wx.ProgressDialog):
881 | '''
882 | 弹出显示框可更新内容显示
883 | '''
884 |
885 | def __init__(self, msg, panel, progress_max = 100):
886 | super(MyProgressDialog, self).__init__(u"提示", msg, progress_max, parent=panel, style=wx.PD_CAN_ABORT|wx.PD_APP_MODAL|wx.PD_AUTO_HIDE)
887 | self._title_msg = msg
888 | self._is_destoryed = False
889 | self._progress_max = progress_max
890 | self._dialog_destory = False
891 |
892 | def on_update_title_msg(self, msg):
893 | self._title_msg = msg
894 |
895 | def on_destory(self):
896 | self._dialog_destory = True
897 |
898 | def on_close(self):
899 | if not self._is_destoryed:
900 | self.Destroy()
901 | self._is_destoryed = True
902 |
903 | def on_update(self):
904 | count = 0
905 | while not self._is_destoryed and count < self._progress_max:
906 | count = (count + 1) % 100
907 | wx.MilliSleep(100)
908 | if self._dialog_destory or not self.Update(count,self._title_msg)[0]:
909 | break
910 | self.on_close()
911 |
912 |
913 | class FileDropTarget(wx.FileDropTarget):
914 | def __init__(self, frame_window, target):
915 | wx.FileDropTarget.__init__(self)
916 | self.frame_window = frame_window
917 | self.target = target
918 |
919 | def OnDropFiles(self, x, y, filepath):
920 |
921 | if self.frame_window._device is None:
922 | self.frame_window.create_tip_dialog(u'未选择设备,请连接设备主机并选择设备!')
923 | return False
924 |
925 | for path in filepath:
926 | path = path.encode('utf-8')
927 | basename = os.path.basename(path)
928 | if path.endswith(('.ipa', '.zip')):
929 | self.frame_window._run_in_work_thread(self.frame_window.on_install, path)
930 | else:
931 | self.frame_window.create_tip_dialog(u'%s格式不符合,请选择.ipa(真机)或者.zip(模拟器)类型安装包' % basename)
932 | return filepath is None
933 |
934 |
935 | class Settings(wx.Dialog):
936 | '''设置系统参数
937 | '''
938 |
939 | def __init__(self, settings_file, *args, **kw):
940 | super(Settings, self).__init__(*args, **kw)
941 | self._settings_file = settings_file
942 | self._settings = {}
943 | self._load_settings()
944 | self._init_dialog()
945 |
946 | def _load_settings(self):
947 | config_parser = ConfigParser.ConfigParser()
948 | config_parser.read(self._settings_file)
949 | try:
950 | qt4i_manage_path = config_parser.get('uispy', 'qt4i-manage')
951 | except ConfigParser.NoOptionError:
952 | qt4i_manage_path = os.path.expanduser("~/Library/Python/2.7/bin/qt4i-manage")
953 | self._settings['qt4i-manage'] = qt4i_manage_path
954 |
955 | def _init_dialog(self):
956 |
957 | vertical_box = wx.BoxSizer(wx.VERTICAL)
958 |
959 | row_box = wx.BoxSizer(wx.HORIZONTAL)
960 |
961 | label = wx.StaticText(self, -1, "qt4i-manage:")
962 | row_box.Add(label, 0, wx.ALIGN_CENTRE | wx.ALL, 5)
963 |
964 | self.tc_qt4i_manage = wx.TextCtrl(self, -1, "", size=(400, -1))
965 | self.tc_qt4i_manage.SetToolTip(u"qt4i-manage的安装路径")
966 | row_box.Add(self.tc_qt4i_manage, 1, wx.ALIGN_CENTRE | wx.ALL, 5)
967 | self.tc_qt4i_manage.SetValue(self._settings['qt4i-manage'])
968 |
969 | vertical_box.Add(row_box, 0, wx.GROW | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 5)
970 |
971 | line = wx.StaticLine(self, -1, size=(20, -1), style=wx.LI_HORIZONTAL)
972 | vertical_box.Add(line, 0, wx.GROW | wx.ALIGN_CENTER_VERTICAL | wx.RIGHT | wx.TOP, 5)
973 |
974 | btnsizer = wx.StdDialogButtonSizer()
975 |
976 | btn_apply = wx.Button(self, wx.ID_APPLY)
977 | btn_apply.SetToolTip(u"设置生效")
978 | btn_apply.SetDefault()
979 | btn_apply.Bind(wx.EVT_BUTTON, self.on_apply_settings)
980 | btnsizer.AddButton(btn_apply)
981 |
982 | btn_cancel = wx.Button(self, wx.ID_CANCEL)
983 | btn_cancel.SetToolTip(u"取消设置")
984 | btnsizer.AddButton(btn_cancel)
985 | btnsizer.Realize()
986 |
987 | vertical_box.Add(btnsizer, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 5)
988 |
989 | self.SetSizer(vertical_box)
990 | vertical_box.Fit(self)
991 |
992 | def on_apply_settings(self, event):
993 | config_parser = ConfigParser.ConfigParser()
994 | config_parser.read(self._settings_file)
995 | config_parser.set("uispy", "qt4i-manage", self.tc_qt4i_manage.GetValue())
996 | with open(self._settings_file, 'w') as fd:
997 | config_parser.write(fd)
998 |
--------------------------------------------------------------------------------