├── .gitignore ├── README.md ├── config ├── app.ico ├── config.py ├── fastbot │ ├── ADBKeyBoard.apk │ ├── abl.strings │ ├── awl.strings │ ├── framework.jar │ ├── max.config │ ├── max.schema.strings │ ├── max.strings │ ├── max.tree.pruning │ ├── max.widget.black │ ├── max.xpath.actions │ └── monkeyq.jar ├── gui.png └── qss_cfg.py ├── favicon.ico ├── gui ├── dialog.py ├── main_window.py ├── right_window.py ├── stacked_adbkit.py ├── stacked_fastbot.py └── stacked_tidevice.py ├── main.py ├── requirements.txt └── utils ├── adbkit.py ├── common.py ├── fastbot_android.py ├── logger.py ├── systemer.py └── tidevice.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .idea 3 | .DS_Store 4 | venv 5 | log 6 | logcat 7 | screen 8 | test 9 | *.pyc 10 | build/ 11 | dist/ 12 | main.spec -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 项目说明 2 | 3 | 使用 PyQt5 打造测试工具 GUI 页面。 4 | - adbuitls:https://github.com/openatx/adbutils 5 | - tidevice:https://github.com/alibaba/taobao-iphone-device 6 | - fastbot-android:https://github.com/bytedance/Fastbot_Android 7 | - fastbot-ios:https://github.com/bytedance/Fastbot_iOS 8 | 9 | ![效果图](config/gui.png) 10 | 11 | # 安装 12 | ```shell 13 | # 工具类 14 | $ pip install adbutils 15 | $ pip install tidevice 16 | 17 | # pyqt 18 | $ pip3 install QtAwesome # 用来显示字体图标 19 | $ pip3 install PyQtWebEngine # 实现窗口内置浏览器页面,相关功能已经删除,不用下载 20 | $ pip3 install pyinstaller # 打包 21 | ``` 22 | 23 | # 打包 24 | 25 | 使用 pyinstaller 打包,在程序入口下,对 main.py 进行打包 26 | 27 | ```shell 28 | $ pyinstaller -F -w main.py 29 | ``` 30 | 31 | - -F:产生单个可执行文件 32 | - -w:取消运行时展示命令行黑框 33 | - -d:debug模式 34 | 35 | 注意: 图片必须使用绝对路径,否则打包不展示 36 | 37 | 打包成功后,会生成下面三个文件 38 | ```shell 39 | build/ 40 | dist/ 41 | main.spec 42 | ``` 43 | 可以将项目中的其他文件(比如图片、.sql数据库文件、yaml配置文件等等),按原来的目录结构复制到dist文件夹中(Mac可能在dist文件夹下有一个以filename命名的文件夹,所有信息在filename文件夹中),如果有缺少文件会导致项目打不开 44 | 45 | 46 | ```shell 47 | # 直接使用这个命令,配置了 icon 和 APP name 48 | $ pyinstaller -F -w main.spec 49 | ``` 50 | 51 | # 问题记录 52 | 53 | 54 | ### Q:为什么 super().__init() 括号会标黄? 55 | A:不是 PyQt 的问题,程序不会出错,是 Pycharm 的提示问题。 56 | 57 | 在上方添加 ```# noinspection PyArgumentList``` 这句注释即可去掉。没强迫症的话就不用管啦。 58 | 59 | Q:为什么绑定事件时,connect 会被标黄? 60 | A:不是 PyQt 的问题,程序不会出错,是 Pycharm 的提示问题。 61 | 62 | 解决:```alt + enter``` 快捷键,在弹框内选择忽略即可,没强迫症的话就不用管啦。 63 | 64 | Q:按钮事件如何绑定? 65 | A:此处有两种绑定方法,需要注意区分使用: 66 | - 绑定的事件不需要传参,写法如下: 67 | ``` 68 | self.left_button_1.clicked.connect(self.open_first_window) 69 | ``` 70 | 提醒一下:自动补全 ```self.open_first_window``` 方法时,后面是带()的,需要去掉。 71 | 72 | 不然会报错:```TypeError: argument 1 has unexpected type 'tuple'```,去掉括号即可。 73 | 74 | 不想去掉括号,使用 ```lambda```表达式也可以,如下。 75 | 76 | - 绑定的事件需要传参,写法如下: 77 | 78 | ``` 79 | self.website_button_1.clicked.connect(lambda: self.__func.open_url(self.__url_cfg.ZENTAO)) 80 | ``` 81 | 这里我调用了一个```open_url(url)```的方法,它需要一个```url```的传参才可以正常运行,此时,就需要用到```lambda```表达式。我们使用 lambda 传递按钮数字给槽,也可以传递任何其他东西---甚至是按钮组件本身。 82 | 83 | 什么是 ```lambda``` 表达式? 84 | 85 | lambda的一般形式是关键字lambda后面跟一个或多个参数,紧跟一个冒号,以后是一个表达式。lambda是一个表达式而不是一个语句。它能够出现在Python语法不允许def出现的地方。作为表达式,lambda返回一个值(即一个新的函数)。lambda用来编写简单的函数,而def用来处理更强大的任务。 86 | 87 | 88 | 89 | # TODO: 90 | idevicecrashreport 工具查看 iOS 崩溃日志:https://testerhome.com/topics/15447 -------------------------------------------------------------------------------- /config/app.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abeelan/mobileTestToolkit/62a59efddb5b35d98040ccb0aa4fb97869ec0d65/config/app.ico -------------------------------------------------------------------------------- /config/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | @Author : lan 5 | @env : Python 3.7.2 6 | @Time : 2019/8/14 4:46 PM 7 | """ 8 | 9 | from utils.systemer import get_abs_path, mkdir 10 | 11 | # 绝对路径 12 | LOGCAT_PATH = get_abs_path("logcat") # 日志路径 13 | SCREEN_PATH = get_abs_path("screen") # 截图路径 14 | FASTBOT_PATH = get_abs_path("config/fastbot") 15 | 16 | # 使用 pyinstall 打包,必须使用绝对路径才能显示出图片来 17 | APP_ICON = get_abs_path("favicon.ico") # APP ICON 18 | 19 | mkdir(LOGCAT_PATH) 20 | mkdir(SCREEN_PATH) 21 | 22 | # fastbot 填写待测包名 23 | PKG_NAME = [ 24 | "com.android.settings", 25 | ] 26 | 27 | # 删除文件夹名称列表 28 | DEL_FOLDER_NAME = [ 29 | "baidu", 30 | ] 31 | 32 | # 执行 fastbot 事件间隔 33 | THROTTLE_LIST = [ 34 | '100', 35 | '200', 36 | '300', 37 | '400', 38 | '500', 39 | ] 40 | 41 | -------------------------------------------------------------------------------- /config/fastbot/ADBKeyBoard.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abeelan/mobileTestToolkit/62a59efddb5b35d98040ccb0aa4fb97869ec0d65/config/fastbot/ADBKeyBoard.apk -------------------------------------------------------------------------------- /config/fastbot/abl.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abeelan/mobileTestToolkit/62a59efddb5b35d98040ccb0aa4fb97869ec0d65/config/fastbot/abl.strings -------------------------------------------------------------------------------- /config/fastbot/awl.strings: -------------------------------------------------------------------------------- 1 | com.esbook.reader.activity.ActAbout 2 | com.esbook.reader.activity.ActBookCover 3 | com.esbook.reader.activity.ActBookCoverInternet 4 | com.esbook.reader.activity.ActBookOver 5 | com.esbook.reader.activity.ActCatalogListCover96 6 | com.esbook.reader.activity.ActCatalogListNovel96 7 | com.esbook.reader.activity.ActCheckBoyOrGirl 8 | com.esbook.reader.activity.ActDiscover 9 | com.esbook.reader.activity.ActDiscoverShake 10 | com.esbook.reader.activity.ActDownloadManager 11 | com.esbook.reader.activity.ActEditNote 12 | com.esbook.reader.activity.ActFeedback 13 | com.esbook.reader.activity.ActFeedbackError 14 | com.esbook.reader.activity.ActForgroundSplashAd 15 | com.esbook.reader.activity.ActGuideNew 16 | com.esbook.reader.activity.ActH5 17 | com.esbook.reader.activity.ActHistory 18 | com.esbook.reader.activity.ActLabelsResult 19 | com.esbook.reader.activity.ActLoading 20 | com.esbook.reader.activity.ActLocalFilesList 21 | com.esbook.reader.activity.ActMultiFindBook 22 | com.esbook.reader.activity.ActMyGold 23 | com.esbook.reader.activity.ActNoticePostDetail 24 | com.esbook.reader.activity.ActNovel 25 | com.esbook.reader.activity.ActOfferWall 26 | com.esbook.reader.activity.ActOnLine 27 | com.esbook.reader.activity.ActPayContinousResult 28 | com.esbook.reader.activity.ActPersonBookShelf 29 | com.esbook.reader.activity.ActPersonNearbyBefore 30 | com.esbook.reader.activity.ActReadCancleAutoBuy 31 | com.esbook.reader.activity.ActReadExtendSetting 32 | com.esbook.reader.activity.ActReward 33 | com.esbook.reader.activity.ActScanFile 34 | com.esbook.reader.activity.ActSearchResultMore 35 | com.esbook.reader.activity.ActSetting 36 | com.esbook.reader.activity.ActTxtCatalogList 37 | com.esbook.reader.activity.CallbackActivity 38 | com.esbook.reader.activity.DownloadAPKActivity 39 | com.esbook.reader.activity.OpenClickActivity 40 | com.esbook.reader.activity.SecondaryWebActivity 41 | com.esbook.reader.activity.frame.ui.ActFragmentContent 42 | com.esbook.reader.activity.search.ActSearchResult 43 | com.esbook.reader.activity.user.ActMobileBinding 44 | com.esbook.reader.activity.user.ActMobileChangeMy 45 | com.esbook.reader.activity.user.ActMobileChangeNew 46 | com.esbook.reader.activity.user.ActMobileChangeVe 47 | com.esbook.reader.activity.user.ActOneKeyLoginMain 48 | com.esbook.reader.activity.user.ActOneKeyLoginPhoneBind 49 | com.esbook.reader.activity.user.ActOneKeyLoginPhoneChange 50 | com.esbook.reader.activity.user.ActOneKeyLoginWelcome 51 | com.esbook.reader.activity.user.ActPassWordFroget 52 | com.esbook.reader.activity.user.ActPasswordReplace 53 | com.esbook.reader.activity.user.ActPasswordReset 54 | com.esbook.reader.activity.user.ActUserLogin 55 | com.esbook.reader.activity.user.ActUserRegister 56 | com.esbook.reader.activity.usercenter.ActUserAccount 57 | com.esbook.reader.activity.usercenter.ActUserCenter 58 | com.esbook.reader.activity.usercenter.ActUserCollect 59 | com.esbook.reader.activity.usercenter.ActUserComment 60 | com.esbook.reader.activity.usercenter.ActUserConsumptionForm 61 | com.esbook.reader.activity.usercenter.ActUserMessageNew 62 | com.esbook.reader.activity.usercenter.ActUserNicknameModify 63 | com.esbook.reader.activity.usercenter.ActUserNote 64 | com.esbook.reader.activity.usercenter.ActUserPayRecord 65 | com.esbook.reader.activity.usercenter.ActUserPost 66 | com.esbook.reader.activity.usercenter.ActUserReadHistory 67 | com.esbook.reader.activity.usercenter.ActUserReplyPost 68 | com.esbook.reader.activity.usercenter.ActUserScoreTask 69 | com.esbook.reader.activity.web.ActAuthorPageWeb 70 | com.esbook.reader.activity.web.ActBookRewardRankWeb 71 | com.esbook.reader.activity.web.ActComicOrListen 72 | com.esbook.reader.activity.web.ActDiscoverWebView 73 | com.esbook.reader.activity.web.ActDuiBaPunchActivity 74 | com.esbook.reader.activity.web.ActEasouGameWeb 75 | com.esbook.reader.activity.web.ActEasouShengMing 76 | com.esbook.reader.activity.web.ActEventDetailWeb 77 | com.esbook.reader.activity.web.ActFindBookDetailWeb 78 | com.esbook.reader.activity.web.ActFindBookThirdWeb 79 | com.esbook.reader.activity.web.ActFriendInvitationWeb 80 | com.esbook.reader.activity.web.ActGameWebView 81 | com.esbook.reader.activity.web.ActGuessYourFavourite 82 | com.esbook.reader.activity.web.ActListBookWeb 83 | com.esbook.reader.activity.web.ActMobileServiceDeclaration 84 | com.esbook.reader.activity.web.ActNewEventsWeb 85 | com.esbook.reader.activity.web.ActNewHotBooks 86 | com.esbook.reader.activity.web.ActNewUserActivity 87 | com.esbook.reader.activity.web.ActNewUserActivityDetail 88 | com.esbook.reader.activity.web.ActNewUserGiftPackageWeb 89 | com.esbook.reader.activity.web.ActNewUserLuckyDrawActivity 90 | com.esbook.reader.activity.web.ActNoChapterWebView 91 | com.esbook.reader.activity.web.ActNoticeWeb 92 | com.esbook.reader.activity.web.ActOverDevicesTipsWeb 93 | com.esbook.reader.activity.web.ActPayVipWebView 94 | com.esbook.reader.activity.web.ActRecommend 95 | com.esbook.reader.activity.web.ActSignInLuckyDrawWeb 96 | com.esbook.reader.activity.web.ActSourceChapterWebView 97 | com.esbook.reader.activity.web.ActTuiAThirdWeb 98 | com.esbook.reader.activity.web.ActUserBookCouponDetail 99 | com.esbook.reader.activity.web.ActUserPayWebView 100 | com.esbook.reader.activity.web.ActUserRewardRankWeb 101 | com.esbook.reader.activity.web.ActUserVipWeb 102 | com.esbook.reader.activity.web.ActUserWeb 103 | com.esbook.reader.activity.web.ActVipMonthlyBookStoreWeb 104 | com.esbook.reader.activity.web.ActVipMonthlyMoreWeb 105 | com.esbook.reader.activity.web.ActWebHelper 106 | com.esbook.reader.activity.web.ActWebHelperContentDetail 107 | com.esbook.reader.activity.web.ActWxSigningWeb 108 | com.esbook.reader.activity.web.ActYouthBindPhone 109 | com.esbook.reader.activity.web.ActYouthMain 110 | com.esbook.reader.activity.web.ActYouthUnBindPhone 111 | com.esbook.reader.activity.web.CustomerServiceWebview 112 | 113 | -------------------------------------------------------------------------------- /config/fastbot/framework.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abeelan/mobileTestToolkit/62a59efddb5b35d98040ccb0aa4fb97869ec0d65/config/fastbot/framework.jar -------------------------------------------------------------------------------- /config/fastbot/max.config: -------------------------------------------------------------------------------- 1 | max.startAfterNSecondsofsleep = 6000 2 | max.wakeupAfterNSecondsofsleep = 4000 3 | max.randomPickFromStringList = true 4 | max.takeScreenshot = false 5 | max.takeScreenshotForEveryStep = false 6 | max.saveGUITreeToXmlEveryStep =false 7 | max.execSchema = true 8 | max.execSchemaEveryStartup = true 9 | max.grantAllPermission = true 10 | -------------------------------------------------------------------------------- /config/fastbot/max.schema.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abeelan/mobileTestToolkit/62a59efddb5b35d98040ccb0aa4fb97869ec0d65/config/fastbot/max.schema.strings -------------------------------------------------------------------------------- /config/fastbot/max.strings: -------------------------------------------------------------------------------- 1 | test 2 | helloworld 3 | 12345 -------------------------------------------------------------------------------- /config/fastbot/max.tree.pruning: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "activity":"com.esbook.reader.activity.web.ActUserVipWeb", 4 | "xpath": "//*[@text='立即开通']", 5 | "resourceid": "", 6 | "contentdesc":"", 7 | "text":"", 8 | "classname":"" 9 | }, 10 | { 11 | "activity":"com.esbook.reader.activity.frame.ui.ActFragmentContent", 12 | "xpath": "//*[@text='立即开通']", 13 | "resourceid": "", 14 | "contentdesc":"", 15 | "text":"", 16 | "classname":"" 17 | }, 18 | { 19 | "activity":"com.esbook.reader.activity.web.ActUserPayWebView", 20 | "xpath": "//*[@text='立即充值']", 21 | "resourceid": "", 22 | "contentdesc":"", 23 | "text":"", 24 | "classname":"" 25 | } 26 | ] -------------------------------------------------------------------------------- /config/fastbot/max.widget.black: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "activity":"com.esbook.reader.activity.web.ActUserVipWeb", 4 | "xpath": "//*[@text='立即开通']" 5 | }, 6 | { 7 | "activity":"com.esbook.reader.activity.frame.ui.ActFragmentContent", 8 | "xpath": "//*[@text='立即开通']" 9 | }, 10 | { 11 | "activity":"com.esbook.reader.activity.web.ActUserPayWebView", 12 | "xpath": "//*[@text='立即充值']" 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /config/fastbot/max.xpath.actions: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "info": "启动APP,点击同意协议", 4 | "prob": 1, 5 | "activity":"com.esbook.reader.activity.ActLoading", 6 | "times": 1, 7 | "actions": [ 8 | { 9 | "xpath":"//*[@resource-id='com.esbook.reader:id/btn_yes']", 10 | "action": "CLICK", 11 | "throttle": 3000 12 | } 13 | ] 14 | }, 15 | { 16 | "info": "书城页面,点击搜索框,允许执行5次", 17 | "prob": 1, 18 | "activity":"com.esbook.reader.activity.frame.ui.ActFragmentContent", 19 | "times": 100, 20 | "actions": [ 21 | { 22 | "xpath":"//*[@resource-id='com.esbook.reader:id/rl_search_view']", 23 | "text": "", 24 | "action": "CLICK", 25 | "throttle": 3000 26 | } 27 | ] 28 | }, 29 | { 30 | "info": "书城页面操作", 31 | "prob": 1, 32 | "activity":"com.esbook.reader.activity.frame.ui.ActFragmentContent", 33 | "times": 10, 34 | "actions": [ 35 | { 36 | "info":"step1:点击我的tab", 37 | "xpath":"//*[@resource-id='com.esbook.reader:id/tabbar']/android.widget.RelativeLayout[5]/android.widget.ImageView[1]", 38 | "action": "CLICK", 39 | "throttle": 3000 40 | }, 41 | { 42 | "info":"step2:点击立即登录按钮", 43 | "xpath":"//*[@resource-id='com.esbook.reader:id/tv_name']", 44 | "action": "CLICK", 45 | "throttle": 3000 46 | } 47 | ] 48 | }, 49 | { 50 | "info": "一键登录页面", 51 | "prob": 1, 52 | "activity":"com.esbook.reader.activity.user.ActOneKeyLoginMain", 53 | "times": 1, 54 | "actions": [ 55 | { 56 | "info":"step1:点击一键登录按钮", 57 | "xpath":"//*[@resource-id='com.esbook.reader:id/ok']", 58 | "action": "CLICK", 59 | "throttle": 3000 60 | }, 61 | { 62 | "info":"step2:点击同意条款", 63 | "xpath":"//*[@resource-id='com.esbook.reader:id/publish_stay']", 64 | "action": "CLICK", 65 | "throttle": 3000 66 | } 67 | ] 68 | }, 69 | { 70 | "info": "手机号登录页面", 71 | "prob": 1, 72 | "activity":"com.esbook.reader.activity.user.ActUserLogin", 73 | "times": 1, 74 | "actions": [ 75 | { 76 | "info":"step1:点击密码登录", 77 | "xpath":"//*[@resource-id='com.esbook.reader:id/tv_password_tab']", 78 | "action": "CLICK", 79 | "throttle": 3000 80 | }, 81 | { 82 | "info":"step2:输入账号", 83 | "xpath":"//*[@resource-id='com.esbook.reader:id/edt_phone']", 84 | "action": "CLICK", 85 | "text":"15011013520", 86 | "throttle": 3000 87 | }, 88 | { 89 | "info":"step3:输入密码", 90 | "xpath":"//*[@resource-id='com.esbook.reader:id/edt_password']", 91 | "action": "CLICK", 92 | "text":"qqqqqq", 93 | "throttle": 3000 94 | }, 95 | { 96 | "info":"step4:点击登录", 97 | "xpath":"//*[@resource-id='com.esbook.reader:id/btn_login']", 98 | "action": "CLICK", 99 | "throttle": 3000 100 | } 101 | ] 102 | } 103 | ] -------------------------------------------------------------------------------- /config/fastbot/monkeyq.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abeelan/mobileTestToolkit/62a59efddb5b35d98040ccb0aa4fb97869ec0d65/config/fastbot/monkeyq.jar -------------------------------------------------------------------------------- /config/gui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abeelan/mobileTestToolkit/62a59efddb5b35d98040ccb0aa4fb97869ec0d65/config/gui.png -------------------------------------------------------------------------------- /config/qss_cfg.py: -------------------------------------------------------------------------------- 1 | """ 2 | @Author : lan 3 | @env : Python 3.7.2 4 | @Time : 2019/8/8 5:14 PM 5 | @Desc : QSS Settings 6 | QSS全称为Qt StyleSheet,是用来控制QT控件的样式表。 7 | 其和Web前段开发中的CSS样式表类似。 8 | """ 9 | 10 | # 按钮颜色设置 11 | BTN_COLOR_GREEN = 'QPushButton{background:#6DDF6D;border-radius:5px;}QPushButton:hover{background:green;}' 12 | BTN_COLOR_GREEN_NIGHT = 'QPushButton{background:#79BDBE;border-radius:5px;}QPushButton:hover{background:#83CDCE;}' 13 | BTN_COLOR_RED = 'QPushButton{background:#F76677;border-radius:5px;}QPushButton:hover{background:red;}' 14 | BTN_COLOR_YELLOW = 'QPushButton{background:#F7D674;border-radius:5px;}QPushButton:hover{background:yellow;}' 15 | BTN_COLOR_BLUE = 'QPushButton{background:#6699FF;border-radius:5px;}QPushButton:hover{background:blue;}' 16 | BTN_COLOR_GRAY = 'QPushButton{background:#CFCFCF;border-radius:5px;}QPushButton:hover{background:#9DF7DA;}' 17 | 18 | # TextEdit 19 | TEXT_EDIT_STYLE = 'background-color: rgb(255, 255, 255);border-radius: 8px; border: 1px groove gray;border-style: outset;' 20 | 21 | # 左侧边栏美化 22 | LEFT_STYLE = ''' 23 | QWidget#left_widget{ 24 | /* 设置背景色 */ 25 | background:gray; 26 | 27 | /* 设置上、下、左三面白边展示 */ 28 | border-top:1px solid white; 29 | border-bottom:1px solid white; 30 | border-left:1px solid white; 31 | 32 | /* 设置左上、左下角圆边展示*/ 33 | border-top-left-radius:10px; 34 | border-bottom-left-radius:10px; 35 | } 36 | 37 | /* 设置按钮无边框,白色展示*/ 38 | QPushButton{ 39 | border:none;color:white; 40 | } 41 | 42 | /* 设置左侧边栏 文本展示*/ 43 | QPushButton#left_label{ 44 | border:none; 45 | border-bottom:1px solid white; 46 | font-size:16px; 47 | font-weight:600; 48 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 49 | } 50 | 51 | QPushButton#left_button:hover{ 52 | border-left:4px solid red; 53 | font-weight:600; 54 | } 55 | ''' 56 | 57 | # 右侧边栏美化 58 | RIGHT_STYLE = ''' 59 | QWidget#right_widget{ 60 | color:#232C51; 61 | background:white; 62 | border-top:1px solid white; 63 | border-bottom:1px solid white; 64 | border-right:1px solid white; 65 | border-top-right-radius:10px; 66 | border-bottom-right-radius:10px; 67 | } 68 | QLabel#right_label{ 69 | border:none; 70 | font-size:16px; 71 | font-weight:700; 72 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 73 | } 74 | 75 | QToolButton{ 76 | border:none; 77 | } 78 | QToolButton:hover{ 79 | border-bottom:2px solid #F76677; 80 | } 81 | ''' 82 | 83 | RIGHT_OTHER_WEBSITE_STYLE = ''' 84 | QPushButton{ 85 | border:none; 86 | color:gray; 87 | font-size:12px; 88 | height:40px; 89 | padding-left:5px; 90 | padding-right:10px; 91 | text-align:left; 92 | } 93 | QPushButton:hover{ 94 | color:black; 95 | border:1px solid #F3F3F5; 96 | border-radius:10px; 97 | background:LightGray; 98 | } 99 | ''' 100 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abeelan/mobileTestToolkit/62a59efddb5b35d98040ccb0aa4fb97869ec0d65/favicon.ico -------------------------------------------------------------------------------- /gui/dialog.py: -------------------------------------------------------------------------------- 1 | """ 2 | @Time : 2021/3/1 7:27 下午 3 | @Author : lan 4 | @Mail : lanzy.nice@gmail.com 5 | @Desc : 6 | """ 7 | 8 | from PyQt5.QtWidgets import * 9 | 10 | import base64 11 | 12 | from PyQt5.QtCore import Qt, QRectF, QSize, pyqtSignal, QTimer 13 | from PyQt5.QtGui import QPixmap, QImage, QPainter, QPainterPath,\ 14 | QColor 15 | from PyQt5.QtWidgets import QWidget, QLabel, QHBoxLayout,\ 16 | QGridLayout, QSpacerItem, QSizePolicy, QGraphicsDropShadowEffect,\ 17 | QListWidget, QListWidgetItem 18 | 19 | 20 | class DiaLog(object): 21 | """普通提示框""" 22 | def __init__(self, widget): 23 | self.widget = widget 24 | self.box = QMessageBox() 25 | 26 | def info(self, body, title=''): 27 | self.box.about(self.widget, title, body) 28 | 29 | def warn(self, body): 30 | self.box.warning(self.widget, 'title', '\n%s' % body) 31 | 32 | def error(self, text, title='错误'): 33 | self.box.critical(self.widget, title, text) 34 | 35 | 36 | class Notice: 37 | """右上角带样式提示框""" 38 | @classmethod 39 | def info(cls, msg): 40 | NotificationWindow().info('~~~ (^_^) ~~~', msg) 41 | 42 | @classmethod 43 | def success(cls, msg): 44 | NotificationWindow().success('~ ╮( ̄▽  ̄)╭ ~', msg) 45 | 46 | @classmethod 47 | def warn(cls, msg): 48 | NotificationWindow().warning('~~ ( O_o ) ~~', msg) 49 | 50 | @classmethod 51 | def error(cls, msg): 52 | NotificationWindow().error('~~~ (T_T) ~~~', msg) 53 | 54 | 55 | class NotificationIcon: 56 | 57 | Info, Success, Warning, Error, Close = range(5) 58 | Types = { 59 | Info: None, 60 | Success: None, 61 | Warning: None, 62 | Error: None, 63 | Close: None 64 | } 65 | 66 | @classmethod 67 | def init(cls): 68 | cls.Types[cls.Info] = QPixmap(QImage.fromData(base64.b64decode('iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAC5ElEQVRYR8VX0VHbQBB9e/bkN3QQU0FMBSEVYFcQ8xPBJLJ1FWAqOMcaxogfTAWQCiAVRKkgTgfmM4zRZu6QhGzL0p0nDPr17e7bt7tv14RX/uiV48MJgAon+8TiAMRtMFogaqUJxADPwRRzg67kl8+xbWJWANR40iPQSSFgtX/mGQkaDr56V3VAKgGos4s2JXwJoF3naMPvMS+SrpTHs032GwGkdF+DsFMVnJm/oyGGeHico0EjIjpYes+YMyVd6R/flfkpBWCCQ9zaZM2LZDfLMGXsZ5kdI/lYBmINgHHyyLd1mWdBbAFAM/GY7K2WYx1AeB4T6L1N9umbGxZ0qktATaEAdCps48D39oq/LwEw3U5CN92LfczJoewfT7MAywDCaEbAuxeLrh0zz4L+0e4aAJfGy+sP3IMxlH1vpMJoSMCJDXgWtJeJVc6ACs9HBBrYODCJAFdYvAmkPJxnNqMwYht7Bn+T/lGg3z4DGEd3RPhQ54DBvwAOVkeqagRXfTLjh+x7+8sALOtfHLuiYzWOAiLoKbD58mnIGbCmLxUepS6NQmYlUGE0JeCTTXT9JvA9E9sZgO5iIpoyc6/YzcqSwQzgGgBXB7oXpH9klpRSkxY1xW/b7Iu2zk34PILPnazCqEPAtTWA8iZ0HsOu9L0bw4DzCJeNocMGNDpQ3IKO+6NUiJ4ysZNiBv5I3zPnmJmG5oM+wbS+9+qkvGi7NAXGmeUy0ioofa+XA0jH0UaMKpdRWs/adcwMqfV/tenqpqHY/Znt+j2gJi00RUzA201dXaxh9iZdZloJS+9H1otrkbRrD5InFqpPskxEshJQ468CkSmJC+i1HigaaxCAuCljgoDhwPdOjf7rFVxxuJrMkXScjtKc1rOLNpJk6nii5XmYzbngzlZn+RIb40kPJPTBYXUt6VEDJ8Pi6bWpNFb/jFYY6YGpDeKdjBmTKdMcxDGEmP73v2a2Gr/NOycGtglQZ/MPzEqCMLGckJEAAAAASUVORK5CYII='))) 69 | cls.Types[cls.Success] = QPixmap(QImage.fromData(base64.b64decode('iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAACZUlEQVRYR8VXS3LTQBDtVsDbcAPMCbB3limkcAKSG4QFdnaYE2BOQLKzxSLJCeAGSUQheSnfwLmB2VJhXmpExpFHI2sk2RWv5FJPv9evP9NieuIfPzE+VSJw8qt3IMDvmahDoDYxt2UAACXMWIIowR5ffn8TJbaBWRE4CXvHAH9RgKXOgQUI48CfXZbZbiTw8Xe/w3d0zkydMkem91IZpyWOJu5sUXS+kEAqt3B+MNOLOuDqDEBLxxFHk7eza5MfIwEJDjhXTYD1s8zinYlEjsCD7FdNI9cJpEq0RFdPR47AMOzLCn69zegz6UgCP+pmfa8RSKudnPNdgCufTOLDxJtdPP7PoA1Cd8HEL5sSUCCD0B0x8bc1f8Bi6sevcgS2VXh6hMOwDz0gsUddNaxWKRjeuKfE/KlJ9Dq4UYH/o/Ns6scj+bgiMAjdayb26xLQwTfVEwg3gRcf6ARq578KuLo7VDc8psCQqwfjr4EfjYvkrAquFJ56UYpdSkAZSmNd1rrg0leOQFELgvA58OJTxVyRaAJORPOpF6UXnFUR5sDiXjs7UqsOMGMRlrWhTkJXpFL3mNrQZhA1lH3F0TiI5FurUQyMpn58VjhkSqQA4Tbw4nSVW6sBU5VXktXSeONlJH3s8jrOVr9RgVSFuNcWfzlh5n3LoKzMAPxxWuiULiQpiR2sZNnCyzIuWUr5Z1Ml0sgdHFZaShVDuR86/0huL3VXtDk/F4e11vKsTHLSCeKx7bYkW80hjLOrV1GhWH0ZrSlyh2MwdZhYfi8oZeYgLBmUiGd8sfVPM6syr2lUSYGaGBuP3QN6rVUwYV/egwAAAABJRU5ErkJggg=='))) 70 | cls.Types[cls.Warning] = QPixmap(QImage.fromData(base64.b64decode('iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAACmElEQVRYR8VXTW7TUBD+xjYSXZFukOIsSE9AskNJJMoJmq4r7OYEwAkabhBOkB/Emt4gVIojdpgbpIumEitX6gKB7UHPkauXxLHfc4F6Z3l+vvnmm/fGhAd+6IHzQwvA9cfOITMfAdQAcx1EdVEAM/tEFADsWyaPn57MfdXClABcT1qnzHSWJiwMzrwgoF91vXGRbS6AH59ajd8hDYmoURQo67tgxoij42rv62KX/04Agu44xmciVMokT32YERgGjquvZ1+y4mQCWPUa0/sk3vQlwqssEFsAVrQbU4XKL/ai2+5PPK6waQ4AOsoDnDARh83NdmwBuJq0fQI9L6p+L7rd3+/5gbAToMPI+FbkIzRRc72mbLcGIFE7jGFRIPHddmZrvstJh1X8CHGv6sxHqe1GkPYCoGcqgcoCAPPCdr2DLQC6wqMoPEj7qdqCNKllxs30sLpjYDluDUDGG5XqhY2sal3w4PiD7c7fJnHShMtJR8zpy/8CALiwndnhBgD1/t+XAXkaZAaUVHwnHulg0W6BNEWlAQD8zna8gQB0Ne70iXCm2j55jCUAei1gxvuaO+uXAcDg7zXHSy640iKUAehOEDJFqDmGQkiPLO5Fv+KADXOqvCuIsrPGsIyQdHou22YeRMJgOdHTQTkAfGk7XrLKrWlAvOhcRgBfWiZ3RQti0zxXuUFXCXMuo0TRitfxugjbIxC5RYzI6s9kIGFh+KLOpiW22id5AUuI8IaisFG4kCQg/sFKJgtPLix3KWXGeRETRbQDuCFCV2spTYMm+2FEI1WBbYIRPTeiqFtqLZeDraaD+qrbkpgQAvfl1WsXU0p/RjIjYYhTkNFgcCVlRlRKoAAc+5aF0V//NVPoc2kTLQZKZ8lx/AMXBmMwuXUwOAAAAABJRU5ErkJggg=='))) 71 | cls.Types[cls.Error] = QPixmap(QImage.fromData(base64.b64decode('iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAACrklEQVRYR82XW27aQBSG/4PtiNhIpStouoImKwjZAV1B07coWCpZQcgK6kh2lLeSFZSsIOwgdAdkBaUSEBQDpxpjU9vM+EJR03nDzJz/mzm3GcIrD3plfZQCeD47O1ho2jERNRmoE9AQG2BgBGBAwIiZe5Zh3JPjiG+5oxCAEF5q2iWITnMtRhOYu5XF4mr/9naYtSYXYGLbHQCXhYVTEwlom657rVqvBOB2uz71/a+ldq1SYe6ahnEhc4sSYGzbfQKOt915eh0D/ZrrnqS/SwEmrVYXRJ92Jb4OC+C65rrtuN0NgIltNwF837V4zN5Hy3V70e9NgFZrCKJ3CQDmJ9MwDsW36XzeB/AhA/CHqeuN2WxWX2paX2JraHneeynA+Pz8lCqVbxLjV5brimxAEJxqiEA8CjZVBvFy+bl2c9MV9hInoAw85qFpGEeRYQVEQjzMokcQHWxsiPne8jzh6j8AodGfyqNlHpiGcaKAkIk/gChwm2yYuv5W2FqfwLNtN5bAQ2bwySB83zENo50A8/1McaFRAU72XVek+mpk+D/JlIKI/xkee654uCbIhjVAqZIrgSgpLhiCwN4OAEj4vEB2yDybBCjsAol4ZD0nRdMQSRcUCsKUeNSw4o2mKMRGEOamoVx8FXDZKVosDYNMUHXAsBRnppo8RQcbpTgIGEkhykpFjnWxzGhPQYxt2yHgS/oIlKVYTJxImpG482nz+VG1Wh1N84pMCCGa0ULXHwmoJwCYnyzPW5fn/68dh7EgPbrMMl3gz7gro+n/7EoWD7w4a96l1NnJ1Yz5Lt6wCgFEk0r1CIkbiPnC9DxH5aHcd4FYGD5MOqVOg/muslh0/vphkm63k5eXZvA0I6qD+ZCI3jDzLxANiHn1NNvb6+30aVYgwLeeUsgFW1svsPA3Ncq4MHzVeO8AAAAASUVORK5CYII='))) 72 | cls.Types[cls.Close] = QPixmap(QImage.fromData(base64.b64decode('iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAeElEQVQ4T2NkoBAwUqifgboGzJy76AIjE3NCWmL0BWwumzV/qcH/f38XpCfHGcDkUVwAUsDw9+8GBmbmAHRDcMlheAGbQnwGYw0DZA1gp+JwFUgKZyDCDQGpwuIlrGGAHHAUGUCRFygKRIqjkeKERE6+oG5eIMcFAOqSchGwiKKAAAAAAElFTkSuQmCC'))) 73 | 74 | @classmethod 75 | def icon(cls, ntype): 76 | return cls.Types.get(ntype) 77 | 78 | 79 | class NotificationItem(QWidget): 80 | 81 | closed = pyqtSignal(QListWidgetItem) 82 | 83 | def __init__(self, title, message, item, *args, ntype=0, callback=None, **kwargs): 84 | super(NotificationItem, self).__init__(*args, **kwargs) 85 | self.item = item 86 | self.callback = callback 87 | layout = QHBoxLayout(self, spacing=0) 88 | layout.setContentsMargins(0, 0, 0, 0) 89 | self.bgWidget = QWidget(self) # 背景控件, 用于支持动画效果 90 | layout.addWidget(self.bgWidget) 91 | 92 | layout = QGridLayout(self.bgWidget) 93 | layout.setHorizontalSpacing(15) 94 | layout.setVerticalSpacing(10) 95 | 96 | # 标题左边图标 97 | layout.addWidget(QLabel(self, pixmap=NotificationIcon.icon(ntype)), 0, 0) 98 | 99 | # 标题 100 | self.labelTitle = QLabel(title, self) 101 | font = self.labelTitle.font() 102 | font.setBold(True) 103 | font.setPixelSize(22) 104 | self.labelTitle.setFont(font) 105 | 106 | # 关闭按钮 107 | self.labelClose = QLabel( 108 | self, cursor=Qt.PointingHandCursor, pixmap=NotificationIcon.icon(NotificationIcon.Close) 109 | ) 110 | 111 | # 消息内容 112 | self.labelMessage = QLabel( 113 | message, self, cursor=Qt.PointingHandCursor, wordWrap=True, alignment=Qt.AlignLeft | Qt.AlignTop 114 | ) 115 | font = self.labelMessage.font() 116 | font.setPixelSize(16) 117 | self.labelMessage.setFont(font) 118 | self.labelMessage.adjustSize() 119 | 120 | # 添加到布局 121 | layout.addWidget(self.labelTitle, 0, 1) 122 | layout.addItem(QSpacerItem( 123 | 40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum), 0, 2) 124 | layout.addWidget(self.labelClose, 0, 3) 125 | layout.addWidget(self.labelMessage, 1, 1, 1, 2) 126 | 127 | # 边框阴影 128 | effect = QGraphicsDropShadowEffect(self) 129 | effect.setBlurRadius(12) 130 | effect.setColor(QColor(0, 0, 0, 25)) 131 | effect.setOffset(0, 2) 132 | self.setGraphicsEffect(effect) 133 | 134 | self.adjustSize() 135 | 136 | # 5秒自动关闭 137 | self._timer = QTimer(self, timeout=self.doClose) 138 | self._timer.setSingleShot(True) # 只触发一次 139 | self._timer.start(3000) 140 | 141 | def doClose(self): 142 | try: 143 | # 可能由于手动点击导致item已经被删除了 144 | self.closed.emit(self.item) 145 | except: 146 | pass 147 | 148 | def showAnimation(self, width): 149 | # 显示动画 150 | pass 151 | 152 | def closeAnimation(self): 153 | # 关闭动画 154 | pass 155 | 156 | def mousePressEvent(self, event): 157 | super(NotificationItem, self).mousePressEvent(event) 158 | w = self.childAt(event.pos()) 159 | if not w: 160 | return 161 | if w == self.labelClose: # 点击关闭图标 162 | # 先尝试停止计时器 163 | self._timer.stop() 164 | self.closed.emit(self.item) 165 | elif w == self.labelMessage and self.callback and callable(self.callback): 166 | # 点击消息内容 167 | self._timer.stop() 168 | self.closed.emit(self.item) 169 | self.callback() # 回调 170 | 171 | def paintEvent(self, event): 172 | # 圆角以及背景色 173 | super(NotificationItem, self).paintEvent(event) 174 | painter = QPainter(self) 175 | path = QPainterPath() 176 | path.addRoundedRect(QRectF(self.rect()), 6, 6) 177 | painter.fillPath(path, Qt.white) 178 | 179 | 180 | class NotificationWindow(QListWidget): 181 | 182 | _instance = None 183 | 184 | def __init__(self, *args, **kwargs): 185 | super(NotificationWindow, self).__init__(*args, **kwargs) 186 | self.setSpacing(20) 187 | self.setMinimumWidth(412) 188 | self.setMaximumWidth(412) 189 | QApplication.instance().setQuitOnLastWindowClosed(True) 190 | # 隐藏任务栏,无边框,置顶等 191 | self.setWindowFlags(self.windowFlags() | Qt.Tool | 192 | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) 193 | # 去掉窗口边框 194 | self.setFrameShape(self.NoFrame) 195 | # 背景透明 196 | self.viewport().setAutoFillBackground(False) 197 | self.setAttribute(Qt.WA_TranslucentBackground, True) 198 | # 不显示滚动条 199 | self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) 200 | self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) 201 | # 获取屏幕高宽 202 | rect = QApplication.instance().desktop().availableGeometry(self) 203 | self.setMinimumHeight(rect.height()) 204 | self.setMaximumHeight(rect.height()) 205 | self.move(rect.width() - self.minimumWidth() - 18, 0) 206 | 207 | def removeItem(self, item): 208 | # 删除item 209 | w = self.itemWidget(item) 210 | self.removeItemWidget(item) 211 | item = self.takeItem(self.indexFromItem(item).row()) 212 | w.close() 213 | w.deleteLater() 214 | del item 215 | self.close() 216 | 217 | @classmethod 218 | def _createInstance(cls): 219 | # 创建实例 220 | if not cls._instance: 221 | cls._instance = NotificationWindow() 222 | cls._instance.show() 223 | NotificationIcon.init() 224 | 225 | @classmethod 226 | def info(cls, title, message, callback=None): 227 | cls._createInstance() 228 | item = QListWidgetItem(cls._instance) 229 | w = NotificationItem(title, message, item, cls._instance, 230 | ntype=NotificationIcon.Info, callback=callback) 231 | w.closed.connect(cls._instance.removeItem) 232 | item.setSizeHint(QSize(cls._instance.width() - 233 | cls._instance.spacing(), w.height())) 234 | cls._instance.setItemWidget(item, w) 235 | 236 | @classmethod 237 | def success(cls, title, message, callback=None): 238 | cls._createInstance() 239 | item = QListWidgetItem(cls._instance) 240 | w = NotificationItem(title, message, item, cls._instance, 241 | ntype=NotificationIcon.Success, callback=callback) 242 | w.closed.connect(cls._instance.removeItem) 243 | item.setSizeHint(QSize(cls._instance.width() - 244 | cls._instance.spacing(), w.height())) 245 | cls._instance.setItemWidget(item, w) 246 | 247 | @classmethod 248 | def warning(cls, title, message, callback=None): 249 | cls._createInstance() 250 | item = QListWidgetItem(cls._instance) 251 | w = NotificationItem(title, message, item, cls._instance, 252 | ntype=NotificationIcon.Warning, callback=callback) 253 | w.closed.connect(cls._instance.removeItem) 254 | item.setSizeHint(QSize(cls._instance.width() - 255 | cls._instance.spacing(), w.height())) 256 | cls._instance.setItemWidget(item, w) 257 | 258 | @classmethod 259 | def error(cls, title, message, callback=None): 260 | cls._createInstance() 261 | item = QListWidgetItem(cls._instance) 262 | w = NotificationItem(title, message, item, 263 | ntype=NotificationIcon.Error, callback=callback) 264 | w.closed.connect(cls._instance.removeItem) 265 | width = cls._instance.width() - cls._instance.spacing() 266 | item.setSizeHint(QSize(width, w.height())) 267 | cls._instance.setItemWidget(item, w) 268 | 269 | 270 | 271 | -------------------------------------------------------------------------------- /gui/main_window.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | @Author : Lan 5 | @env : Python 3.7.2 6 | @Time : 2019/8/8 5:14 PM 7 | @Desc : 主窗口实现 8 | """ 9 | import os 10 | import sys 11 | import qtawesome 12 | 13 | from PyQt5.QtWidgets import * 14 | from PyQt5.QtCore import * 15 | from PyQt5.QtGui import * 16 | 17 | from config import qss_cfg, config 18 | from gui.right_window import RightStacked 19 | 20 | 21 | class MainWindow(QMainWindow): 22 | def __init__(self): 23 | super(MainWindow, self).__init__() 24 | 25 | """ 26 | -------------- 27 | | 1. 窗口设置 | 28 | -------------- 29 | """ 30 | self.setWindowTitle("测试工具") 31 | self.setFixedSize(900, 700) # 设置窗体大小 32 | self.setWindowOpacity(0.96) # 设置窗口透明度 33 | self.setAttribute(Qt.WA_TranslucentBackground) # 设置窗口背景透明 34 | self.setWindowFlag(Qt.FramelessWindowHint) # 隐藏边框 35 | 36 | """ 37 | -------------- 38 | | 2. 布局设置 | 39 | -------------- 40 | """ 41 | '''2.1 创建主窗口部件''' 42 | self.main_widget = QWidget() # 创建窗口主部件 43 | self.main_layout = QGridLayout() # 创建主部件的网格布局 44 | self.main_layout.setSpacing(0) # 设置左右侧部件间隙为0 45 | self.main_widget.setLayout(self.main_layout) # 设置窗口主部件布局为网格布局 46 | 47 | '''2.2 创建左侧边栏部件''' 48 | self.left_sidebar = QWidget() # 创建左侧部件 49 | self.left_sidebar.setObjectName('left_widget') 50 | 51 | # self.left_layout = QGridLayout() # 创建左侧部件的网格布局层 52 | self.left_layout = QVBoxLayout() # 创建左侧部件的网格布局层 53 | self.left_sidebar.setLayout(self.left_layout) # 设置左侧部件布局为网格 54 | self.left_sidebar.setStyleSheet(qss_cfg.LEFT_STYLE) # 设置左侧部件美化 55 | 56 | '''2.3 将左右侧部件添加到主窗口部件内''' 57 | self.right_stacked = RightStacked().right_stacked 58 | 59 | self.main_layout.addWidget(self.left_sidebar, 0, 0) # 左侧部件在第0行第0列,占8行3列 60 | self.main_layout.addWidget(self.right_stacked, 0, 4) # 右侧部件在第0行第3列,占8行9列 61 | self.setCentralWidget(self.main_widget) # 设置窗口主部件 62 | 63 | """ 64 | ----------------- 65 | | 3. 左侧边栏设置 | 66 | ----------------- 67 | """ 68 | '''3.1 左上角 三个按钮''' 69 | # 创建按钮 按钮内文案为空 70 | self.btn_close = QPushButton("") # 关闭 71 | self.btn_minimized = QPushButton("") # 最小化 72 | self.btn_fullscreen = QPushButton("") # 最大化 73 | # 设置按钮尺寸 74 | for btn in (self.btn_close, 75 | self.btn_minimized, 76 | self.btn_fullscreen): 77 | btn.setFixedSize(13, 13) 78 | # 设置按钮提示语 79 | self.btn_close.setToolTip("关闭窗口") 80 | self.btn_minimized.setToolTip("最小化暂不可用") 81 | self.btn_fullscreen.setToolTip("最大化暂不可用") 82 | # 设置按钮颜色 83 | self.btn_close.setStyleSheet(qss_cfg.BTN_COLOR_RED) 84 | self.btn_minimized.setStyleSheet(qss_cfg.BTN_COLOR_YELLOW) 85 | self.btn_fullscreen.setStyleSheet(qss_cfg.BTN_COLOR_GREEN) 86 | 87 | '''3.2 分类大标题设置''' 88 | self.left_label_android = QPushButton(qtawesome.icon('fa.android', color='white'), " AdbKit") 89 | self.left_label_ios = QPushButton(qtawesome.icon('fa.apple', color='white'), " iOSKit") 90 | self.left_label_fastbot = QPushButton(qtawesome.icon('fa.rocket', color='white'), " FastBot") 91 | 92 | for label in ( 93 | self.left_label_android, 94 | self.left_label_ios, 95 | self.left_label_fastbot 96 | ): 97 | label.setObjectName('left_label') 98 | 99 | 100 | self.btn_layout = QHBoxLayout() 101 | self.btn_layout.addWidget(self.btn_close) 102 | self.btn_layout.addWidget(self.btn_minimized) 103 | self.btn_layout.addWidget(self.btn_fullscreen) 104 | 105 | self.left_layout.addLayout(self.btn_layout) 106 | self.left_layout.addStretch(3) 107 | self.left_layout.addWidget(self.left_label_android) 108 | self.left_layout.addStretch(1) 109 | self.left_layout.addWidget(self.left_label_ios) 110 | self.left_layout.addStretch(1) 111 | self.left_layout.addWidget(self.left_label_fastbot) 112 | self.left_layout.addStretch(10) 113 | 114 | """3.4 左侧边栏按钮事件绑定""" 115 | self.btn_close.clicked.connect(QCoreApplication.instance().quit) 116 | self.btn_minimized.clicked.connect(lambda: self.clicked_btn_minimized()) 117 | self.btn_fullscreen.clicked.connect(lambda: self.clicked_btn_fullscreen()) 118 | 119 | self.left_label_android.clicked.connect(lambda: self.switch_adbkit()) 120 | self.left_label_ios.clicked.connect(lambda: self.switch_ios()) 121 | self.left_label_fastbot.clicked.connect(lambda: self.switch_fastbot()) 122 | 123 | """ 124 | -------------- 125 | | 点击后切换页面 | 126 | -------------- 127 | """ 128 | 129 | def switch_adbkit(self): 130 | self.right_stacked.setCurrentIndex(0) 131 | 132 | def switch_ios(self): 133 | self.right_stacked.setCurrentIndex(1) 134 | 135 | def switch_fastbot(self): 136 | self.right_stacked.setCurrentIndex(2) 137 | 138 | def clicked_btn_minimized(self): 139 | """窗口最小化""" 140 | # self.showMinimized() 141 | pass 142 | 143 | def clicked_btn_fullscreen(self): 144 | """窗口最大化""" 145 | # self.showFullScreen() 146 | # self.btn_fullscreen.clicked.connect(lambda: NotificationWindow.info('提示', '这是一条会自动关闭的消息')) 147 | pass 148 | 149 | """ 150 | ----------------------- 151 | | 重写鼠标移动事件 152 | | 目的:支持无边框窗体移动 153 | ----------------------- 154 | """ 155 | def mousePressEvent(self, event): 156 | if event.button() == Qt.LeftButton: 157 | self.m_flag = True 158 | self.m_Position = event.globalPos() - self.pos() # 获取鼠标相对窗口的位置 159 | event.accept() 160 | self.setCursor(QCursor(Qt.OpenHandCursor)) # 更改鼠标图标 161 | 162 | def mouseMoveEvent(self, QMouseEvent): 163 | if Qt.LeftButton and self.m_flag: 164 | self.move(QMouseEvent.globalPos() - self.m_Position) # 更改窗口位置 165 | QMouseEvent.accept() 166 | 167 | def mouseReleaseEvent(self, QMouseEvent): 168 | self.m_flag = False 169 | self.setCursor(QCursor(Qt.ArrowCursor)) 170 | 171 | 172 | def main(): 173 | app = QApplication(sys.argv) 174 | app.setWindowIcon(QIcon(QPixmap(config.APP_ICON))) # 设置 app icon 175 | gui = MainWindow() 176 | gui.setWindowTitle("MobileToolKit") 177 | gui.show() 178 | sys.exit(app.exec_()) 179 | -------------------------------------------------------------------------------- /gui/right_window.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | @Author : Lan 5 | @env : Python 3.7.2 6 | @Time : 2019/9/20 1:56 PM 7 | @Desc : 8 | """ 9 | 10 | from PyQt5.QtWidgets import * 11 | from gui.stacked_adbkit import AdbKitPage 12 | from gui.stacked_fastbot import Fastbot 13 | from gui.stacked_tidevice import TiDevicePage 14 | 15 | 16 | class RightStacked(object): 17 | def __init__(self): 18 | self.right_stacked = QStackedWidget() 19 | 20 | stack1 = AdbKitPage() 21 | stack2 = TiDevicePage() 22 | stack3 = Fastbot() 23 | 24 | self.right_stacked.addWidget(stack1.widget) 25 | self.right_stacked.addWidget(stack2.widget) 26 | self.right_stacked.addWidget(stack3.widget) 27 | 28 | self.right_stacked.setCurrentIndex(0) # 设置默认界面 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /gui/stacked_adbkit.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | @Author : Lan 5 | @env : Python 3.7.2 6 | @Time : 2019/9/23 5:35 PM 7 | @Desc : 8 | """ 9 | import os 10 | import logging 11 | import threading 12 | import pyperclip 13 | import functools 14 | 15 | from PyQt5.QtWidgets import * 16 | from PyQt5.QtCore import * 17 | from PyQt5.QtGui import * 18 | from adbutils import adb 19 | 20 | from utils.adbkit import AdbKit 21 | from utils import common 22 | from utils import systemer 23 | from config import qss_cfg, config 24 | from gui.dialog import DiaLog, Notice 25 | 26 | 27 | def check_device(func): 28 | """设备检测,判断仅有设备时继续执行""" 29 | @functools.wraps(func) 30 | def wrapper(*args, **kw): 31 | if len(AdbKit.device_list()) == 0: 32 | Notice().error("当前设备为空,AdbKit 初始化失败!") 33 | return 34 | return func(*args, **kw) 35 | return wrapper 36 | 37 | 38 | class AdbKitPage: 39 | def __init__(self): 40 | self.widget = QWidget() # 创建页面布局 41 | self.widget.setObjectName('right_widget') 42 | self.widget.setStyleSheet(qss_cfg.RIGHT_STYLE) # 设置右侧部件美化 43 | 44 | self.layout = QVBoxLayout(self.widget) 45 | self.dialog = DiaLog(self.widget) 46 | self.notice = Notice() 47 | 48 | self.init_ui() 49 | self.add_event() 50 | 51 | def init_ui(self): 52 | """设备选择区域""" 53 | self.label_device_choose = QLabel("请选择设备:") 54 | 55 | self.cmb_device_choose = QComboBox() 56 | self.cmb_device_choose.setFixedSize(200, 30) 57 | self.cmb_device_choose.addItems(AdbKit.device_list()) # 下拉框添加数据 58 | 59 | self.btn_refresh_device = QPushButton('刷新') 60 | self.btn_refresh_device.setFixedSize(40, 20) 61 | self.btn_refresh_device.setStyleSheet(qss_cfg.BTN_COLOR_YELLOW) 62 | 63 | self.layout_device_choose = QHBoxLayout() 64 | self.layout_device_choose.addWidget(self.label_device_choose) 65 | self.layout_device_choose.addWidget(self.cmb_device_choose) 66 | self.layout_device_choose.addWidget(self.btn_refresh_device) 67 | self.layout_device_choose.addStretch(1) 68 | 69 | """按钮区域""" 70 | # 标签 71 | self.label_device = QLabel("设备操作") 72 | self.label_device.setObjectName('right_label') 73 | self.layout.addWidget(self.label_device) 74 | 75 | # 设备信息 76 | self.btn_device_info = QPushButton('设备信息') 77 | self.btn_wifi_connect = QPushButton('无线连接') 78 | self.btn_kill_adb = QPushButton('Kill adbd') 79 | 80 | self.edit_ip = QLineEdit() 81 | self.edit_port = QLineEdit() 82 | self.edit_ip.setPlaceholderText('IP') 83 | self.edit_port.setPlaceholderText('5555') 84 | self.edit_port.setFixedSize(50, 25) 85 | self.edit_ip.setStyleSheet(qss_cfg.TEXT_EDIT_STYLE) 86 | self.edit_port.setStyleSheet(qss_cfg.TEXT_EDIT_STYLE) 87 | # self.edit_ip.setInputMask('000.000.000.000;') # 校验 IP 88 | # self.edit_port.setInputMask('00000;') 89 | self.edit_port.setMaxLength(5) # 最大为5位 90 | 91 | self.btn_device_info.setFixedSize(100, 30) 92 | self.btn_wifi_connect.setFixedSize(100, 30) 93 | self.btn_kill_adb.setFixedSize(100, 30) 94 | 95 | self.btn_device_info.setStyleSheet(qss_cfg.BTN_COLOR_GREEN_NIGHT) 96 | self.btn_wifi_connect.setStyleSheet(qss_cfg.BTN_COLOR_GREEN_NIGHT) 97 | self.btn_kill_adb.setStyleSheet(qss_cfg.BTN_COLOR_GREEN_NIGHT) 98 | 99 | layout_00 = QHBoxLayout() 100 | layout_00.addWidget(self.btn_device_info) 101 | layout_00.addWidget(self.btn_kill_adb) 102 | layout_00.addWidget(self.btn_wifi_connect) 103 | layout_00.addWidget(self.edit_ip) 104 | layout_00.addWidget(QLabel(":")) 105 | layout_00.addWidget(self.edit_port) 106 | layout_00.addStretch(1) 107 | 108 | # 手机代理 109 | self.btn_open_proxy = QPushButton('开启代理') 110 | self.btn_close_proxy = QPushButton('关闭代理') 111 | self.edit_hostname_port = QLineEdit() 112 | self.edit_hostname_port.setPlaceholderText('hostname:port') 113 | self.edit_hostname_port.setFixedSize(200, 25) 114 | self.edit_hostname_port.setStyleSheet(qss_cfg.TEXT_EDIT_STYLE) 115 | self.edit_port.setMaxLength(20) # 最大为5位 116 | 117 | layout_proxy = QHBoxLayout() 118 | layout_proxy.addWidget(self.btn_open_proxy) 119 | layout_proxy.addWidget(self.btn_close_proxy) 120 | layout_proxy.addWidget(self.edit_hostname_port) 121 | layout_proxy.addStretch(1) 122 | 123 | # 安装APK 124 | self.btn_install = QPushButton('安装应用') 125 | self.btn_choose_apk = QPushButton('选择APK') 126 | self.btn_qrcode = QPushButton('生成二维码') 127 | self.edit_apk_path = QLineEdit() 128 | 129 | self.edit_apk_path.setPlaceholderText('请粘贴安装包下载链接 或 选择路径~') 130 | self.edit_apk_path.setStyleSheet(qss_cfg.TEXT_EDIT_STYLE) 131 | self.edit_apk_path.setFixedSize(400, 25) 132 | 133 | self.btn_install.setToolTip('复制需要安装的「 APK URL 」点击文本框。\n安装过程见IDE内的 DEBUG 日志。') 134 | self.btn_choose_apk.setFixedSize(70, 20) 135 | self.btn_qrcode.setFixedSize(70, 20) 136 | 137 | self.btn_install.setStyleSheet(qss_cfg.BTN_COLOR_GREEN) 138 | self.btn_choose_apk.setStyleSheet(qss_cfg.BTN_COLOR_YELLOW) 139 | self.btn_qrcode.setStyleSheet(qss_cfg.BTN_COLOR_YELLOW) 140 | 141 | layout_01 = QHBoxLayout() 142 | layout_01.addWidget(self.btn_install) 143 | layout_01.addWidget(self.edit_apk_path) 144 | layout_01.addWidget(self.btn_choose_apk) 145 | layout_01.addWidget(self.btn_qrcode) 146 | layout_01.addStretch(1) 147 | 148 | # 打开网页 149 | self.btn_open_browser = QPushButton('打开网页') 150 | 151 | self.edit_open_url = QLineEdit() 152 | self.edit_open_url.setFixedSize(500, 25) 153 | self.edit_open_url.setStyleSheet(qss_cfg.TEXT_EDIT_STYLE) 154 | self.edit_open_url.setPlaceholderText('请输入链接') 155 | 156 | layout_02 = QHBoxLayout() 157 | layout_02.addWidget(self.btn_open_browser) 158 | layout_02.addWidget(self.edit_open_url) 159 | layout_02.addStretch(1) 160 | 161 | # 发送文本 162 | self.btn_send_text = QPushButton('发送文本') 163 | self.btn_send_text.setToolTip('不支持中文') 164 | 165 | self.edit_send_text = QLineEdit() 166 | self.edit_send_text.setFixedSize(500, 25) 167 | self.edit_send_text.setStyleSheet(qss_cfg.TEXT_EDIT_STYLE) 168 | self.edit_send_text.setPlaceholderText('请输入内容') 169 | 170 | layout_03 = QHBoxLayout() 171 | layout_03.addWidget(self.btn_send_text) 172 | layout_03.addWidget(self.edit_send_text) 173 | layout_03.addStretch(1) 174 | 175 | # 删除设备上的指定文件夹 176 | self.btn_del_folder = QPushButton('删除文件夹') 177 | self.btn_insert = QPushButton('添加') 178 | self.btn_delete = QPushButton('删除') 179 | 180 | self.btn_insert.setFixedSize(70, 20) 181 | self.btn_delete.setFixedSize(70, 20) 182 | self.btn_insert.setStyleSheet(qss_cfg.BTN_COLOR_YELLOW) 183 | self.btn_delete.setStyleSheet(qss_cfg.BTN_COLOR_YELLOW) 184 | 185 | self.edit_insert_folder = QLineEdit() 186 | self.edit_insert_folder.setFixedSize(100, 20) 187 | self.edit_insert_folder.setStyleSheet(qss_cfg.TEXT_EDIT_STYLE) 188 | 189 | self.combox_choose_folder = QComboBox() # 设置文件下拉框 190 | self.combox_choose_folder.setFixedSize(150, 30) 191 | self.combox_choose_folder.addItems(config.DEL_FOLDER_NAME) # 添加数据到下拉框 192 | 193 | layout_04 = QHBoxLayout() 194 | layout_04.addWidget(self.btn_del_folder) 195 | layout_04.addWidget(self.combox_choose_folder) 196 | layout_04.addWidget(self.btn_delete) 197 | layout_04.addWidget(self.btn_insert) 198 | layout_04.addWidget(self.edit_insert_folder) 199 | layout_04.addStretch(1) 200 | 201 | # 针对当前包的操作 202 | self.btn_get_package_info = QPushButton('应用包详情') 203 | self.btn_get_package_name = QPushButton('应用包名') 204 | self.btn_reset_app = QPushButton('重置当前应用') 205 | self.btn_uninstall_app = QPushButton('卸载当前应用') 206 | 207 | layout_05 = QHBoxLayout() 208 | layout_05.addWidget(self.btn_get_package_info) 209 | layout_05.addWidget(self.btn_get_package_name) 210 | layout_05.addWidget(self.btn_reset_app) 211 | layout_05.addWidget(self.btn_uninstall_app) 212 | layout_05.addStretch(1) 213 | 214 | # 滑动操作 line 6 215 | self.btn_swipe_up = QPushButton('上滑') 216 | self.btn_swipe_down = QPushButton('下滑') 217 | self.btn_swipe_left = QPushButton('左滑') 218 | self.btn_swipe_right = QPushButton('右滑') 219 | self.btn_swipe_stop = QPushButton('停止') 220 | 221 | self.check_box = QCheckBox('是否连续') # 复选框 222 | 223 | self.btn_swipe_stop.setFixedSize(70, 20) 224 | self.btn_swipe_stop.setStyleSheet(qss_cfg.BTN_COLOR_YELLOW) 225 | 226 | layout_06 = QHBoxLayout() 227 | layout_06.addWidget(self.btn_swipe_up) 228 | layout_06.addWidget(self.btn_swipe_down) 229 | layout_06.addWidget(self.btn_swipe_left) 230 | layout_06.addWidget(self.btn_swipe_right) 231 | layout_06.addWidget(self.check_box) 232 | layout_06.addWidget(self.btn_swipe_stop) 233 | layout_06.addStretch(1) 234 | 235 | # 获取日志 line 7 236 | self.btn_clear_log = QPushButton('清空日志') 237 | self.btn_logcat = QPushButton('获取日志') 238 | self.btn_open_log_path = QPushButton('打开目录') 239 | 240 | self.edit_logcat_filename = QLineEdit() 241 | self.edit_logcat_filename.setFixedSize(275, 25) 242 | self.edit_logcat_filename.setStyleSheet(qss_cfg.TEXT_EDIT_STYLE) 243 | self.edit_logcat_filename.setPlaceholderText('日志名称(获取成功后在该处展示名称)') 244 | self.edit_logcat_filename.setReadOnly(True) 245 | 246 | layout_07 = QHBoxLayout() 247 | layout_07.addWidget(self.btn_clear_log) 248 | layout_07.addWidget(self.btn_logcat) 249 | layout_07.addWidget(self.btn_open_log_path) 250 | layout_07.addWidget(self.edit_logcat_filename) 251 | layout_07.addStretch(1) 252 | 253 | # 截图 line 8 254 | self.btn_screenshot = QPushButton('截图') 255 | self.btn_open_screenshot = QPushButton('打开截图') 256 | self.btn_open_screenshot_dir = QPushButton('打开目录') 257 | 258 | layout_08 = QHBoxLayout() 259 | layout_08.addWidget(self.btn_screenshot) 260 | layout_08.addWidget(self.btn_open_screenshot) 261 | layout_08.addWidget(self.btn_open_screenshot_dir) 262 | layout_08.addStretch(1) 263 | 264 | """设置页面通用按钮大小和颜色""" 265 | btn_list = [ 266 | self.btn_install, 267 | self.btn_open_browser, 268 | self.btn_send_text, 269 | self.btn_del_folder, 270 | self.btn_get_package_info, self.btn_get_package_name, self.btn_reset_app, 271 | self.btn_uninstall_app, 272 | self.btn_swipe_up, self.btn_swipe_down, self.btn_swipe_left, self.btn_swipe_right, 273 | self.btn_clear_log, self.btn_logcat, self.btn_open_log_path, 274 | self.btn_screenshot, self.btn_open_screenshot, self.btn_open_screenshot_dir, 275 | self.btn_open_proxy, self.btn_close_proxy, 276 | ] 277 | for btn in btn_list: 278 | btn.setFixedSize(100, 30) 279 | btn.setStyleSheet(qss_cfg.BTN_COLOR_GREEN_NIGHT) 280 | 281 | '''添加各部件到主布局''' 282 | self.layout.addStretch(2) 283 | self.layout.addLayout(self.layout_device_choose) 284 | self.layout.addStretch(1) 285 | self.layout.addWidget(self.label_device) 286 | self.layout.addLayout(layout_00) # 设备信息 287 | self.layout.addLayout(layout_proxy) # 设置代理 288 | self.layout.addLayout(layout_01) # 安装 289 | self.layout.addLayout(layout_05) # 获取信息 290 | self.layout.addLayout(layout_04) # 删除文件夹 291 | self.layout.addLayout(layout_02) # 打开网页 292 | self.layout.addLayout(layout_03) # 发送文本 293 | self.layout.addLayout(layout_07) # 清空日志 294 | self.layout.addLayout(layout_08) # 截图 295 | self.layout.addLayout(layout_06) # 滑动 296 | self.layout.addStretch(2) 297 | 298 | def add_event(self): 299 | # 设备选择框 300 | self.cmb_device_choose.currentIndexChanged.connect(lambda: self.current_device()) 301 | self.btn_refresh_device.clicked.connect(lambda: self.clicked_devices_check()) 302 | 303 | # 设备信息 无线连接 304 | self.btn_device_info.clicked.connect(lambda: self.clicked_get_device_info()) 305 | self.btn_wifi_connect.clicked.connect(lambda: self.clicked_connect_wifi()) 306 | self.btn_kill_adb.clicked.connect(lambda: self.clicked_server_kill()) 307 | 308 | # 设置代理 309 | self.btn_open_proxy.clicked.connect(lambda: self.clicked_open_proxy()) 310 | self.btn_close_proxy.clicked.connect(lambda: self.clicked_close_proxy()) 311 | 312 | # 安装应用 313 | self.btn_install.clicked.connect(lambda: self.clicked_btn_install()) 314 | self.btn_choose_apk.clicked.connect(lambda: self.clicked_btn_choose_apk_path()) 315 | self.btn_qrcode.clicked.connect(lambda: self.clicked_btn_qrcode()) 316 | 317 | # 打开网页 318 | self.btn_open_browser.clicked.connect(lambda: self.clicked_btn_open_browser()) 319 | 320 | # 发送文本 321 | self.btn_send_text.clicked.connect(lambda: self.clicked_btn_send_text()) 322 | 323 | # 删除文件夹 324 | self.btn_del_folder.clicked.connect(lambda: self.clicked_btn_del_folder()) 325 | self.btn_delete.clicked.connect(lambda: self.clicked_btn_delete()) 326 | self.btn_insert.clicked.connect(lambda: self.clicked_btn_insert()) 327 | 328 | # 设备相关的其他操作 - 获取包名版本号等 329 | self.btn_get_package_info.clicked.connect(lambda: self.clicked_btn_get_pkg_info()) 330 | self.btn_get_package_name.clicked.connect(lambda: self.clicked_btn_get_package_name()) 331 | self.btn_reset_app.clicked.connect(lambda: self.clicked_btn_reset_current_app()) 332 | self.btn_uninstall_app.clicked.connect(lambda: self.clicked_btn_uninstall_current_app()) 333 | 334 | # 日志操作 335 | self.btn_clear_log.clicked.connect(lambda: self.clicked_btn_logcat_c()) 336 | self.btn_logcat.clicked.connect(lambda: self.clicked_btn_logcat()) 337 | self.btn_open_log_path.clicked.connect(lambda: self.clicked_btn_open_log_path()) 338 | 339 | # 截图 340 | self.btn_screenshot.clicked.connect(lambda: self.clicked_btn_screenshot()) 341 | self.btn_open_screenshot.clicked.connect(lambda: self.clicked_btn_open_screenshot_path()) 342 | self.btn_open_screenshot_dir.clicked.connect(lambda: self.clicked_btn_open_screenshot_dir()) 343 | 344 | # 滑动 345 | self.btn_swipe_up.clicked.connect(lambda: self.clicked_btn_swipe2up()) 346 | self.btn_swipe_down.clicked.connect(lambda: self.clicked_btn_swipe2down()) 347 | self.btn_swipe_left.clicked.connect(lambda: self.clicked_btn_swipe2left()) 348 | self.btn_swipe_right.clicked.connect(lambda: self.clicked_btn_swipe2right()) 349 | self.btn_swipe_stop.clicked.connect(lambda: self.clicked_btn_swipe_stop()) 350 | 351 | @check_device 352 | def adb(self): 353 | return AdbKit(self.current_device()) 354 | 355 | def current_device(self): 356 | """获取当前列表选中的设备""" 357 | device = self.cmb_device_choose.currentText() 358 | # logging.info(f"current device: {None if not device else device}") 359 | return device 360 | 361 | def clicked_devices_check(self): 362 | """刷新设备列表""" 363 | devices_list = AdbKit.device_list() 364 | self.cmb_device_choose.clear() 365 | self.cmb_device_choose.addItems(devices_list) 366 | self.cmb_device_choose.setCurrentIndex(0) 367 | logging.info(f"Device checking... Now device list: {devices_list}") 368 | # self.dialog.info(f"Now device list: \n{devices_list}") 369 | 370 | def clicked_get_device_info(self): 371 | """获取设备信息""" 372 | if self.adb(): 373 | output = self.adb().device_info_complete() 374 | self.dialog.info(output) 375 | 376 | def clicked_connect_wifi(self): 377 | if self.adb(): 378 | # 首先获取 LineEdit 写入的值 379 | edit_ip_value = self.edit_ip.text() 380 | edit_port_value = self.edit_port.text() 381 | port = 5555 if not edit_port_value else edit_port_value 382 | 383 | output = self.adb().connect(edit_ip_value, port) 384 | 385 | if "Successful" in output: 386 | self.clicked_devices_check() 387 | self.edit_ip.setText(self.adb().ip()) 388 | self.notice.info(output) 389 | 390 | def clicked_server_kill(self): 391 | output = adb.server_kill() 392 | self.notice.info(f"success: adb server killed.") 393 | 394 | def clicked_open_proxy(self): 395 | content = self.edit_hostname_port.text().strip() 396 | host, port = content.split(":") if content else (common.get_pc_ip(), 8888) 397 | 398 | if self.adb(): 399 | self.adb().open_proxy(host, port) 400 | 401 | def clicked_close_proxy(self): 402 | if self.adb(): 403 | self.adb().close_proxy() 404 | 405 | def paste_apk_url(self): 406 | """在文本框内粘贴剪切板的内容 407 | TODO:想实现点击文本框,直接自动粘贴剪切版的内容(点击文本框的信号获取处理不回,暂时搁置) 408 | """ 409 | try: 410 | url = pyperclip.paste() 411 | logging.info(f"剪贴板内容为:{url}") 412 | self.edit_apk_path.setText(url) 413 | except Exception as e: 414 | logging.info('The shear plate is empty! %s' % e) 415 | 416 | def get_apk_path_choose_text(self): 417 | """获取安装apk文本框的内容""" 418 | try: 419 | text = self.edit_apk_path.text().strip() 420 | if text: 421 | logging.info(f"安装链接:{text}") 422 | return self.edit_apk_path.text() 423 | raise ValueError 424 | except ValueError: 425 | self.notice.warn("给一下安装路径或链接呗~") 426 | logging.warning('安装路径或链接为空') 427 | except Exception as e: 428 | self.notice.error("完蛋了,走到异常分支了!") 429 | logging.error('获取 apk 文本框内容失败~ %s' % e) 430 | 431 | def clicked_btn_install(self): 432 | """点击 安装应用 按钮""" 433 | if self.adb() and self.get_apk_path_choose_text(): 434 | try: 435 | t = threading.Thread( 436 | target=self.adb().install, 437 | args=(self.get_apk_path_choose_text(),) 438 | ) 439 | t.start() 440 | except Exception as e: 441 | self.notice.error(f"安装失败\n{e}") 442 | 443 | def clicked_btn_choose_apk_path(self): 444 | """点击 选择APK 按钮,通过 apk 文件选择框 选择安装路径,""" 445 | # 对话框的文件扩展名过滤器 filter,设置多个文件扩展名过滤,使用双引号隔开; 446 | # “All Files(*);;PDF Files(*.pdf);;Text Files(*.txt)” 447 | open_path = QFileDialog() 448 | path = open_path.getOpenFileName(filter='APK Files(*.apk);;') 449 | self.edit_apk_path.setText(path[0]) 450 | 451 | def clicked_btn_qrcode(self): 452 | """点击生成二维码""" 453 | text = self.get_apk_path_choose_text() 454 | if text: 455 | try: 456 | os.system(f'open {AdbKit().qr_code(text)}') 457 | except Exception: 458 | os.system(f'start explorer {AdbKit().qr_code(text)}') 459 | else: 460 | logging.info("文本为空,不生成二维码!") 461 | # self.notice.error("文本框内容为空,不生成二维码!") 462 | 463 | def clicked_btn_open_browser(self): 464 | """点击【打开网页】""" 465 | if self.adb(): 466 | url = self.edit_open_url.text() 467 | logging.info(f"当前输入框内链接为:{None if not url else url}") 468 | if not url: 469 | logging.info("打开网页:链接为空,请输入后再试...") 470 | self.notice.error('不给链接怎么打的开呀~') 471 | return 472 | # self.adb().start_web_page(url) 473 | self.adb().adb_device.open_browser(url) 474 | 475 | def clicked_btn_send_text(self): 476 | """点击【发送文本】""" 477 | if self.adb(): 478 | text = self.edit_send_text.text().strip() 479 | if not text: 480 | logging.info("输入文本:文本为空,请输入后再试...") 481 | self.notice.warn('文本框得有内容的呀~') 482 | return 483 | logging.info(f"当前文本输入框内容为:{text}") 484 | self.adb().adb_device.send_keys(text) 485 | 486 | def clicked_btn_del_folder(self): 487 | """点击【删除文件夹】""" 488 | if self.adb(): 489 | folder_name = self.combox_choose_folder.currentText() 490 | self.adb().delete_folder(folder_name) 491 | logging.info(f"删除文件夹:{folder_name}") 492 | self.notice.info(f"删除文件夹成功:{folder_name}") 493 | 494 | def clicked_btn_delete(self): 495 | """删除下拉框内的文件名称数据""" 496 | folder_name = self.combox_choose_folder.currentText() 497 | config.DEL_FOLDER_NAME.remove(folder_name) 498 | self.combox_choose_folder.clear() 499 | self.combox_choose_folder.addItems(config.DEL_FOLDER_NAME) 500 | logging.info(f"Delete folder name:{folder_name}") 501 | 502 | def clicked_btn_insert(self): 503 | """增加下拉框内的文件名称数据""" 504 | folder_name = self.edit_insert_folder.text() 505 | config.DEL_FOLDER_NAME.append(folder_name) 506 | self.combox_choose_folder.clear() 507 | self.combox_choose_folder.addItems(config.DEL_FOLDER_NAME) 508 | logging.info(f"Insert folder name:{folder_name}") 509 | 510 | def clicked_btn_get_pkg_info(self): 511 | """点击【获取当前包信息】""" 512 | if self.adb(): 513 | info_dict = self.adb().current_app_info() 514 | output = f"版本名: {info_dict['version_name']}\n" \ 515 | f"版本号: {info_dict['version_code']}\n\n" \ 516 | f"首次安装时间: {info_dict['first_install_time']}\n" \ 517 | f"最后更新时间: {info_dict['last_update_time']}" 518 | logging.info(f"Get Package Info...") 519 | self.dialog.info(output) 520 | 521 | def clicked_btn_get_package_name(self): 522 | """点击【获取当前包名和activity】""" 523 | if self.adb(): 524 | current_app = self.adb().current_app() 525 | activity = f"{current_app[0]}/{current_app[-1]}" 526 | logging.info(f"Activity ==> {activity}") 527 | self.dialog.info(activity) 528 | 529 | def clicked_btn_reset_current_app(self): 530 | """点击【重置当前应用】""" 531 | try: 532 | self.adb().current_app_reset() 533 | except Exception as e: 534 | self.notice.error(f"重置当前应用出错了:{e}") 535 | 536 | def clicked_btn_uninstall_current_app(self): 537 | """点击【卸载当前应用】""" 538 | try: 539 | pkg = self.adb().current_app()[0] 540 | self.adb().uninstall(pkg) 541 | logging.info(f'卸载当前应用 >> {pkg}') 542 | except Exception as e: 543 | self.notice.error(f"卸载当前应用出错了:{e}") 544 | 545 | def _swipe(self, func): 546 | """判断是否连续滑动,供调用拉起线程""" 547 | if self.check_box.isChecked(): 548 | while True: 549 | func() 550 | else: 551 | func() 552 | 553 | def clicked_btn_swipe2up(self): 554 | """点击【上滑】如果勾选框为勾选状态,则连续上滑""" 555 | if self.adb(): 556 | self.t = threading.Thread(target=self._swipe, args=(self.adb().swipe_to_up,)) 557 | self.t.start() 558 | 559 | def clicked_btn_swipe2down(self): 560 | """点击【下拉】如果勾选框为勾选状态,则连续下拉""" 561 | if self.adb(): 562 | self.t = threading.Thread(target=self._swipe, args=(self.adb().swipe_to_down,)) 563 | self.t.start() 564 | 565 | def clicked_btn_swipe2left(self): 566 | """点击【左滑】如果勾选框为勾选状态,则连续下拉""" 567 | if self.adb(): 568 | self.t = threading.Thread(target=self._swipe, args=(self.adb().swipe_to_left,)) 569 | self.t.start() 570 | 571 | def clicked_btn_swipe2right(self): 572 | """点击【左滑】如果勾选框为勾选状态,则连续下拉""" 573 | if self.adb(): 574 | self.t = threading.Thread(target=self._swipe, args=(self.adb().swipe_to_right,)) 575 | self.t.start() 576 | 577 | def clicked_btn_swipe_stop(self): 578 | """点击【停止】结束连续滑动""" 579 | try: 580 | common.stop_thread(self.t) 581 | logging.info("已停止连续滑动") 582 | self.notice.success("已停止连续滑动") 583 | except Exception as e: 584 | logging.error(f"停止连续滑动线程失败: {e}") 585 | self.dialog.error(f"当前无运行中线程\n停止连续滑动线程失败: {e}") 586 | 587 | def clicked_btn_logcat_c(self): 588 | """logcat -c""" 589 | if self.adb(): 590 | self.adb().logcat_c() 591 | 592 | def clicked_btn_logcat(self): 593 | """获取日志""" 594 | log_path = config.LOGCAT_PATH 595 | # filename = self.adb().gen_file_name("log") 596 | # file_path = os.path.join(log_path, filename) 597 | # self.adb().logcat(file_path) 598 | if self.adb(): 599 | self.edit_logcat_filename.setText(self.adb().dump_crash_log(log_path)) 600 | 601 | def clicked_btn_open_log_path(self): 602 | """打开log路径""" 603 | log_path = config.LOGCAT_PATH 604 | systemer.open_path(log_path) 605 | 606 | def clicked_btn_screenshot(self): 607 | """截图""" 608 | if self.adb(): 609 | t = threading.Thread(target=self.adb().screenshot, args=(config.SCREEN_PATH,)) 610 | t.start() 611 | t.join() 612 | self.notice.info('截图完成') 613 | 614 | def clicked_btn_open_screenshot_path(self): 615 | """打开截图""" 616 | filename = systemer.get_latest_file(config.SCREEN_PATH) 617 | if not systemer.open_path(filename): 618 | self.notice.warn("打开失败,当前目录为空!") 619 | 620 | def clicked_btn_open_screenshot_dir(self): 621 | """打开截图目录""" 622 | systemer.open_path(config.SCREEN_PATH) 623 | 624 | -------------------------------------------------------------------------------- /gui/stacked_fastbot.py: -------------------------------------------------------------------------------- 1 | """ 2 | @Author : Lan 3 | @env : Python 3.7.2 4 | @Time : 2021/8/6 10:00 AM 5 | @Desc : 6 | """ 7 | import logging 8 | import threading 9 | 10 | from PyQt5.QtWidgets import * 11 | from PyQt5.QtGui import * 12 | from PyQt5.QtCore import * 13 | from config import qss_cfg, config 14 | from gui.dialog import DiaLog, Notice 15 | from gui.stacked_adbkit import AdbKitPage 16 | from gui.stacked_tidevice import TiDevicePage 17 | from utils.adbkit import AdbKit 18 | from utils.fastbot_android import FastbotAndroid 19 | from utils.tidevice import TiDevice 20 | 21 | 22 | class Fastbot(object): 23 | def __init__(self): 24 | 25 | self.widget = QWidget() 26 | self.layout = QVBoxLayout(self.widget) 27 | self.dialog = DiaLog(self.widget) 28 | self.notice = Notice() 29 | 30 | self.widget.setObjectName('right_widget') 31 | self.widget.setStyleSheet(qss_cfg.RIGHT_STYLE) # 设置右侧部件美化 32 | 33 | self.init_ui() 34 | self.add_event() 35 | 36 | def init_ui(self): 37 | # Package 38 | self.label_package = QLabel("Package:") 39 | self.combox_package = QComboBox() # 包名下拉框 40 | self.combox_package.setFixedSize(200, 25) 41 | self.combox_package.addItems(config.PKG_NAME) 42 | 43 | self.layout_package = QHBoxLayout() 44 | self.layout_package.addStretch(1) 45 | self.layout_package.addWidget(self.label_package) 46 | self.layout_package.addWidget(self.combox_package) 47 | self.layout_package.addStretch(1) 48 | 49 | # Duration 50 | self.label_duration = QLabel("Duration:") 51 | self.edit_duration = QLineEdit() 52 | self.edit_duration.setStyleSheet(qss_cfg.TEXT_EDIT_STYLE) 53 | self.edit_duration.setFixedSize(200, 25) 54 | 55 | self.layout_duration = QHBoxLayout() 56 | self.layout_duration.addStretch(1) 57 | self.layout_duration.addWidget(self.label_duration) 58 | self.layout_duration.addWidget(self.edit_duration) 59 | self.layout_duration.addStretch(1) 60 | 61 | # Throttle 62 | self.label_throttle = QLabel("Throttle:") 63 | self.combox_throttle = QComboBox() 64 | self.combox_throttle.setFixedSize(200, 25) 65 | self.combox_throttle.addItems(config.THROTTLE_LIST) 66 | self.combox_throttle.setCurrentIndex(3) 67 | 68 | self.layout_throttle = QHBoxLayout() 69 | self.layout_throttle.addStretch(1) 70 | self.layout_throttle.addWidget(self.label_throttle) 71 | self.layout_throttle.addWidget(self.combox_throttle) 72 | self.layout_throttle.addStretch(1) 73 | 74 | # output dir 75 | self.label_output = QLabel("安卓设备日志目录:") 76 | self.edit_output = QLineEdit() 77 | self.edit_output.setFixedSize(200, 25) 78 | self.edit_output.setStyleSheet(qss_cfg.TEXT_EDIT_STYLE) 79 | self.edit_output.setText("sdcard/fastbot") 80 | 81 | self.layout_output = QHBoxLayout() 82 | self.layout_output.addStretch(1) 83 | self.layout_output.addWidget(self.label_output) 84 | self.layout_output.addWidget(self.edit_output) 85 | self.layout_output.addStretch(1) 86 | 87 | # android btn 88 | self.btn_android = QPushButton("Android Runner") 89 | self.btn_android.setFixedSize(150, 30) 90 | self.btn_android.setStyleSheet(qss_cfg.BTN_COLOR_GREEN_NIGHT) 91 | 92 | # ios btn 93 | self.btn_ios = QPushButton("iOS Runner") 94 | self.btn_ios.setFixedSize(150, 30) 95 | self.btn_ios.setStyleSheet(qss_cfg.BTN_COLOR_GREEN_NIGHT) 96 | 97 | self.layout_btn = QHBoxLayout() 98 | self.layout_btn.addStretch(1) 99 | self.layout_btn.addWidget(self.btn_android) 100 | self.layout_btn.addWidget(self.btn_ios) 101 | self.layout_btn.addStretch(1) 102 | 103 | # 添加到参数布局内 104 | self.layout_settings = QVBoxLayout() 105 | self.layout_settings.addStretch(1) 106 | self.layout_settings.addLayout(self.layout_package) 107 | self.layout_settings.addStretch(1) 108 | self.layout_settings.addLayout(self.layout_duration) 109 | self.layout_settings.addStretch(1) 110 | self.layout_settings.addLayout(self.layout_throttle) 111 | self.layout_settings.addStretch(1) 112 | self.layout_settings.addLayout(self.layout_output) 113 | self.layout_settings.addStretch(2) 114 | self.layout_settings.addLayout(self.layout_btn) 115 | self.layout_settings.addStretch(2) 116 | 117 | # 创建控制台布局 118 | self.edit_console = QTextEdit() 119 | self.edit_console.append(f"日志目录: {config.LOGCAT_PATH}") 120 | self.edit_console.append(f"安卓如果出现闪退或者 OOM,日志会自动推送到该目录下!") 121 | self.edit_console.append(f"苹果运行日志会自动存储在该目录下,需要人工查看是否出错!") 122 | self.edit_console.append(f"TODO:抱歉,由于多线程之前互相调用原因,这里暂时还不能实时输出日志。") 123 | self.edit_console.setReadOnly(True) 124 | self.edit_console.setStyleSheet(qss_cfg.TEXT_EDIT_STYLE) 125 | 126 | self.layout_console = QVBoxLayout() 127 | self.layout_console.addWidget(self.edit_console) 128 | 129 | # 将参数布局 和 控制台布局 添加到总布局 130 | self.layout.addLayout(self.layout_settings) 131 | self.layout.addLayout(self.layout_console) 132 | 133 | def add_event(self): 134 | if AdbKitPage().current_device(): 135 | self.btn_android.clicked.connect(lambda: threading.Thread(target=self.android_fastbot().run).start()) 136 | else: 137 | self.btn_android.clicked.connect(lambda: self.notice.error("Can't find any android device/emulator")) 138 | 139 | if TiDevicePage().current_device(): 140 | self.btn_ios.clicked.connect(lambda: threading.Thread(target=self.ios_fastbot).start()) 141 | else: 142 | self.btn_ios.clicked.connect(lambda: self.notice.error("Can't find any iOS device/emulator")) 143 | 144 | 145 | def android_fastbot(self): 146 | package = self.combox_package.currentText() 147 | duration = self.edit_duration.text() 148 | throttle = self.combox_throttle.currentText() 149 | output = self.edit_output.text() 150 | 151 | if AdbKitPage().adb(): 152 | self.edit_console.append("Fastbot - Android - Running ...") 153 | 154 | try: 155 | fastbot = FastbotAndroid( 156 | package=package, 157 | duration=duration, 158 | throttle=throttle, 159 | output=output 160 | ) 161 | # fastbot.run() 162 | return fastbot 163 | except Exception as e: 164 | self.dialog.error(e) 165 | 166 | def ios_fastbot(self): 167 | package = self.combox_package.currentText() 168 | duration = self.edit_duration.text() 169 | throttle = self.combox_throttle.currentText() 170 | 171 | self.edit_console.append("Fastbot - iOS - Running ...") 172 | 173 | try: 174 | TiDevice().fastbot_and_logcat( 175 | bundle_id=package, 176 | duration=duration, 177 | throttle=throttle, 178 | ) 179 | except Exception as e: 180 | self.dialog.error(e) 181 | 182 | -------------------------------------------------------------------------------- /gui/stacked_tidevice.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | @Author : Lan 5 | @env : Python 3.7.2 6 | @Time : 2019/9/25 10:00 AM 7 | @Desc : 8 | """ 9 | import os 10 | import logging 11 | import threading 12 | 13 | from PyQt5.QtWidgets import * 14 | from PyQt5.QtCore import * 15 | from PyQt5.QtGui import * 16 | 17 | from gui.dialog import DiaLog, Notice 18 | 19 | from utils import common, systemer 20 | from utils.common import MyThread 21 | from utils.tidevice import TiDevice 22 | from config import qss_cfg, config 23 | 24 | 25 | class TiDevicePage: 26 | def __init__(self): 27 | self.widget = QWidget() # 创建页面布局 28 | self.widget.setObjectName('right_widget') 29 | self.widget.setStyleSheet(qss_cfg.RIGHT_STYLE) # 设置右侧部件美化 30 | 31 | self.layout = QVBoxLayout(self.widget) 32 | self.notice = Notice() 33 | 34 | self.init_ui() 35 | self.add_event() 36 | 37 | def init_ui(self): 38 | """""" 39 | """1. 设备选择区域""" 40 | self.label_device_choose = QLabel("连接设备列表:") 41 | 42 | self.cmb_device_choose = QComboBox() 43 | self.cmb_device_choose.setFixedSize(200, 30) 44 | # self.cmb_device_choose.addItems(self.device_list()) # 下拉框添加数据 45 | 46 | self.btn_refresh_device = QPushButton('刷新') 47 | self.btn_refresh_device.setFixedSize(40, 20) 48 | self.btn_refresh_device.setStyleSheet(qss_cfg.BTN_COLOR_YELLOW) 49 | 50 | self.layout_device_choose = QHBoxLayout() 51 | self.layout_device_choose.addWidget(self.label_device_choose) 52 | self.layout_device_choose.addWidget(self.cmb_device_choose) 53 | self.layout_device_choose.addWidget(self.btn_refresh_device) 54 | self.layout_device_choose.addStretch(1) 55 | 56 | """2. 获取设备信息按钮区域""" 57 | # 标签 58 | self.label_device = QLabel("设备操作") 59 | self.label_device.setObjectName('right_label') 60 | self.layout.addWidget(self.label_device) 61 | 62 | # 设备信息 63 | self.btn_device_info = QPushButton('设备信息') 64 | self.btn_device_info.setFixedSize(100, 30) 65 | self.btn_device_info.setStyleSheet(qss_cfg.BTN_COLOR_GREEN_NIGHT) 66 | 67 | # 安装列表 68 | self.btn_install_app_list = QPushButton("安装应用列表") 69 | self.btn_install_app_list.setFixedSize(100, 30) 70 | self.btn_install_app_list.setStyleSheet(qss_cfg.BTN_COLOR_GREEN_NIGHT) 71 | 72 | layout_00 = QHBoxLayout() 73 | layout_00.addWidget(self.btn_device_info) 74 | layout_00.addWidget(self.btn_install_app_list) 75 | layout_00.addStretch(1) 76 | 77 | # 安装APK 78 | self.btn_install = QPushButton('安装应用') 79 | self.btn_choose_apk = QPushButton('选择IPA') 80 | self.btn_qrcode = QPushButton('生成二维码') 81 | self.edit_apk_path = QLineEdit() 82 | 83 | self.btn_install.setToolTip('复制包链接 或者 选择路径') 84 | self.edit_apk_path.setPlaceholderText('请粘贴安装包下载链接 或 选择路径~') 85 | self.edit_apk_path.setFixedSize(400, 25) 86 | self.edit_apk_path.setStyleSheet(qss_cfg.TEXT_EDIT_STYLE) 87 | self.btn_choose_apk.setFixedSize(70, 20) 88 | self.btn_qrcode.setFixedSize(70, 20) 89 | 90 | self.btn_install.setStyleSheet(qss_cfg.BTN_COLOR_GREEN) 91 | self.btn_choose_apk.setStyleSheet(qss_cfg.BTN_COLOR_YELLOW) 92 | self.btn_qrcode.setStyleSheet(qss_cfg.BTN_COLOR_YELLOW) 93 | 94 | layout_01 = QHBoxLayout() 95 | layout_01.addWidget(self.btn_install) 96 | layout_01.addWidget(self.edit_apk_path) 97 | layout_01.addWidget(self.btn_choose_apk) 98 | layout_01.addWidget(self.btn_qrcode) 99 | layout_01.addStretch(1) 100 | 101 | # 包 启动、杀死进程、卸载 102 | self.btn_uninstall_app = QPushButton('卸载应用') 103 | self.btn_start_app = QPushButton('启动应用') 104 | self.btn_kill_app = QPushButton('杀死应用') 105 | 106 | self.edit_pkg_name = QLineEdit() 107 | self.edit_pkg_name.setFixedSize(160, 25) 108 | self.edit_pkg_name.setPlaceholderText('com.easou.esbook') 109 | self.edit_pkg_name.setText('com.easou.esbook') 110 | self.edit_pkg_name.setStyleSheet(qss_cfg.TEXT_EDIT_STYLE) 111 | 112 | layout_02 = QHBoxLayout() 113 | layout_02.addWidget(self.btn_uninstall_app) 114 | layout_02.addWidget(self.btn_start_app) 115 | layout_02.addWidget(self.btn_kill_app) 116 | layout_02.addWidget(self.edit_pkg_name) 117 | layout_02.addStretch(1) 118 | 119 | # 截图 120 | self.btn_screenshot = QPushButton('截图') 121 | self.btn_open_screenshot = QPushButton('打开截图') 122 | self.btn_open_screenshot_dir = QPushButton('打开目录') 123 | 124 | layout_03 = QHBoxLayout() 125 | layout_03.addWidget(self.btn_screenshot) 126 | layout_03.addWidget(self.btn_open_screenshot) 127 | layout_03.addWidget(self.btn_open_screenshot_dir) 128 | layout_03.addStretch(1) 129 | 130 | # 输出台 131 | self.label_output = QLabel("输出") 132 | self.label_output.setObjectName('right_label') 133 | self.layout.addWidget(self.label_output) 134 | 135 | self.edit_output = QTextEdit() 136 | self.edit_output.setReadOnly(True) # 只读 137 | self.edit_output.setStyleSheet(qss_cfg.TEXT_EDIT_STYLE) 138 | self.edit_output.resize(600, 500) 139 | 140 | layout_04 = QHBoxLayout() 141 | layout_04.addWidget(self.edit_output) 142 | 143 | """设置页面通用按钮大小和颜色""" 144 | btn_list = [ 145 | self.btn_install, 146 | self.btn_uninstall_app, self.btn_start_app, self.btn_kill_app, 147 | self.btn_screenshot, self.btn_open_screenshot, self.btn_open_screenshot_dir, 148 | ] 149 | for btn in btn_list: 150 | btn.setFixedSize(100, 30) 151 | btn.setStyleSheet(qss_cfg.BTN_COLOR_GREEN_NIGHT) 152 | 153 | '''添加各部件到主布局''' 154 | self.layout.addStretch(1) 155 | self.layout.addLayout(self.layout_device_choose) 156 | self.layout.addStretch(1) 157 | self.layout.addWidget(self.label_device) 158 | self.layout.addLayout(layout_00) # 设备信息 159 | self.layout.addLayout(layout_01) # 安装 160 | self.layout.addLayout(layout_02) # 打开网页 161 | self.layout.addLayout(layout_03) # 截图 162 | self.layout.addStretch(1) 163 | self.layout.addWidget(self.label_output) # 输入控制台信息 164 | self.layout.addLayout(layout_04) # 输入控制台信息 165 | self.layout.addStretch(1) 166 | 167 | def add_event(self): 168 | # 设备选择框 169 | self.cmb_device_choose.currentIndexChanged.connect(lambda: self.current_device()) 170 | self.btn_refresh_device.clicked.connect(lambda: self.clicked_devices_check()) 171 | 172 | # 获取设备信息 173 | self.btn_device_info.clicked.connect(lambda: self.clicked_get_device_info()) 174 | self.btn_install_app_list.clicked.connect(lambda: self.clicked_get_app_list()) 175 | 176 | # 安装应用 177 | self.btn_install.clicked.connect(lambda: self.clicked_btn_install()) 178 | self.btn_choose_apk.clicked.connect(lambda: self.clicked_btn_choose_apk_path()) 179 | self.btn_qrcode.clicked.connect(lambda: self.clicked_btn_qrcode()) 180 | 181 | # 卸载 启动 杀掉 182 | self.btn_uninstall_app.clicked.connect(lambda: self.clicked_btn_uninsatll()) 183 | self.btn_start_app.clicked.connect(lambda: self.clicked_btn_start_app()) 184 | self.btn_kill_app.clicked.connect(lambda: self.clicked_btn_kill_app()) 185 | 186 | # 截图 187 | self.btn_screenshot.clicked.connect(lambda: self.clicked_btn_screenshot()) 188 | self.btn_open_screenshot.clicked.connect(lambda: self.clicked_btn_open_screenshot_path()) 189 | self.btn_open_screenshot_dir.clicked.connect(lambda: self.clicked_btn_open_screenshot_dir()) 190 | 191 | def device(self): 192 | d = TiDevice() 193 | if len(d.list()) == 0: 194 | self.edit_output.append("设备为空,TiDevice 初始化失败!") 195 | self.edit_output.moveCursor(QTextCursor.End) 196 | self.notice.error("当前设备为空,TiDevice 初始化失败!") 197 | return 198 | return d 199 | 200 | def device_list(self): 201 | if self.device(): 202 | return self.device().list(name=True) 203 | return [] 204 | 205 | def current_device(self): 206 | """获取当前列表选中的设备""" 207 | device = self.cmb_device_choose.currentText() 208 | # logging.info(f"current device: {None if not device else device}") 209 | return device 210 | 211 | def clicked_devices_check(self): 212 | """刷新设备列表""" 213 | if self.device(): 214 | devices_list = self.device().list(name=True) 215 | self.cmb_device_choose.clear() 216 | self.cmb_device_choose.addItems(devices_list) 217 | self.cmb_device_choose.setCurrentIndex(0) 218 | logging.info(f"Device checking... Now device list: {devices_list}") 219 | self.edit_output.append(f"Device checking... \nNow device list: {devices_list}") 220 | self.edit_output.moveCursor(QTextCursor.End) 221 | 222 | def clicked_get_device_info(self): 223 | """获取设备信息""" 224 | if self.device(): 225 | output = "\n".join(self.device().info()) 226 | self.edit_output.setText(output) 227 | 228 | def clicked_get_app_list(self): 229 | """获取已安装应用列表""" 230 | if self.device(): 231 | output = "\n".join(self.device().app_list()) 232 | self.edit_output.setText(output) 233 | 234 | def clicked_btn_install(self): 235 | """点击 安装应用 按钮""" 236 | text = self.edit_apk_path.text() 237 | if text.isspace() or len(text) == 0: 238 | info = "请输入安装包链接或本地路径~" 239 | logging.info(info) 240 | self.notice.warn(info) 241 | return 242 | 243 | info = f"安装链接:{text}" 244 | logging.info(info) 245 | self.edit_output.append(info) 246 | self.edit_output.moveCursor(QTextCursor.End) 247 | 248 | if self.device(): 249 | try: 250 | # t = threading.Thread(target=self.device().install, ) 251 | t = MyThread(self.device().install, args=(text,)) 252 | t.start() 253 | self.edit_output.setText("已启动子线程进行安装...请稍等...") 254 | t.join() 255 | self.edit_output.append(f"安装完成, 结果为 {t.get_result()}.") 256 | except Exception as e: 257 | logging.info(f"安装失败, {e}.") 258 | self.notice.error(f"安装失败, {e}.") 259 | 260 | def clicked_btn_uninsatll(self): 261 | if self.device(): 262 | pkg_name = self.edit_pkg_name.text() 263 | info = f"卸载应用:{pkg_name}" 264 | self.edit_output.append(info) 265 | logging.info(info) 266 | output = self.device().uninstall(pkg_name) 267 | self.edit_output.append("\n".join(output)) 268 | self.edit_output.moveCursor(QTextCursor.End) 269 | 270 | def clicked_btn_start_app(self): 271 | if self.device(): 272 | pkg_name = self.edit_pkg_name.text() 273 | info = f"启动应用:{pkg_name}" 274 | self.edit_output.append(info) 275 | logging.info(info) 276 | output = self.device().launch(pkg_name) 277 | self.edit_output.append("\n".join(output)) 278 | self.edit_output.moveCursor(QTextCursor.End) 279 | 280 | def clicked_btn_kill_app(self): 281 | if self.device(): 282 | pkg_name = self.edit_pkg_name.text() 283 | info = f"关闭应用:{pkg_name}" 284 | self.edit_output.append(info) 285 | logging.info(info) 286 | output = self.device().kill(pkg_name) 287 | self.edit_output.append("\n".join(output)) 288 | self.edit_output.moveCursor(QTextCursor.End) 289 | 290 | def clicked_btn_choose_apk_path(self): 291 | """点击 选择 IPA 按钮,通过 apk 文件选择框 选择安装路径,""" 292 | # 对话框的文件扩展名过滤器 filter,设置多个文件扩展名过滤,使用双引号隔开; 293 | # “All Files(*);;PDF Files(*.pdf);;Text Files(*.txt)” 294 | open_path = QFileDialog() 295 | path = open_path.getOpenFileName(filter='IPA Files(*.ipa);;') 296 | self.edit_apk_path.setText(path[0]) 297 | 298 | def clicked_btn_qrcode(self): 299 | """点击生成二维码""" 300 | text = self.edit_apk_path.text() 301 | if text: 302 | try: 303 | os.system(f'open {common.qr_code(text)}') 304 | except OSError: 305 | os.system(f'start explorer {common.qr_code(text)}') 306 | else: 307 | logging.info("文本为空,不生成二维码!") 308 | self.notice.info("文本框内容为空,不生成二维码!") 309 | 310 | def clicked_btn_screenshot(self): 311 | """截图""" 312 | # t = threading.Thread(target=self.device().screenshot, args=(config.SCREEN_PATH,)) 313 | if self.device(): 314 | t = threading.Thread(target=self.device().screenshot) 315 | t.start() 316 | self.edit_output.append("正在截图中... 请稍等...") 317 | t.join() 318 | self.edit_output.append("截图完成") 319 | self.notice.success('截图完成') 320 | self.edit_output.moveCursor(QTextCursor.End) 321 | 322 | def clicked_btn_open_screenshot_path(self): 323 | """打开截图""" 324 | filename = systemer.get_latest_file(config.SCREEN_PATH) 325 | if not systemer.open_path(filename): 326 | self.notice.warn("打开失败,当前目录为空!") 327 | 328 | self.edit_output.append(f"[ OPEN ] ==> {filename}") 329 | self.edit_output.moveCursor(QTextCursor.End) 330 | 331 | def clicked_btn_open_screenshot_dir(self): 332 | """打开截图目录""" 333 | screen_path = config.SCREEN_PATH 334 | systemer.open_path(screen_path) 335 | self.edit_output.append(f"[ OPEN ] ==> {screen_path}") 336 | self.edit_output.moveCursor(QTextCursor.End) 337 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | @Author : Lan 5 | @env : Python 3.7.2 6 | @Time : 2019/9/25 2:34 PM 7 | @Desc : 主窗口展示运行入口 8 | 9 | """ 10 | import logging 11 | from utils.logger import Logger 12 | from gui import main_window 13 | 14 | 15 | if __name__ == '__main__': 16 | main_window.main() 17 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | colorlog==4.0.2 2 | qtawesome==1.0.3 3 | PyQt5==5.13.0 4 | pyperclip==1.7.0 5 | adbutils==0.8.2 6 | MyQR==2.3.1 7 | PyYAML==5.3 8 | tidevice==0.4.17 9 | -------------------------------------------------------------------------------- /utils/adbkit.py: -------------------------------------------------------------------------------- 1 | """ 2 | @Time : 2021/3/1 11:15 上午 3 | @Author : lan 4 | @Mail : lanzy.nice@gmail.com 5 | @Desc : 6 | 7 | TODO: 提交 pr, wlan_ip() 里面没有做无权限设备获取IP的处理,可以添加上 8 | """ 9 | import re 10 | import os 11 | import logging 12 | import platform 13 | import datetime 14 | import subprocess 15 | from random import random 16 | 17 | import adbutils 18 | import functools 19 | from MyQR import myqr 20 | from time import sleep 21 | from adbutils import adb 22 | 23 | from utils import systemer 24 | 25 | 26 | def check_device(func): 27 | """设备检测,判断仅有设备时继续执行""" 28 | @functools.wraps(func) 29 | def wrapper(*args, **kw): 30 | device_list = AdbKit.device_list() 31 | logging.debug(f"设备连接列表:{device_list}") 32 | if len(device_list) == 0: 33 | logging.error("没有设备") 34 | raise RuntimeError("Can't find any android device/emulator") 35 | return func(*args, **kw) 36 | return wrapper 37 | 38 | 39 | class AdbKit: 40 | """ 41 | https://github.com/openatx/adbutils 42 | https://developer.android.com/studio/command-line/adb 43 | """ 44 | def __init__(self, serial=None): 45 | self.serial = serial 46 | self.adb = adb 47 | self.adb_path = adbutils.adb_path() 48 | self.adb_device = adb.device(self.serial) 49 | 50 | self.shell = systemer.shell 51 | logging.debug(f"AdbKit(serial={serial})") 52 | 53 | def __adb_client(self): 54 | """AdbClient() API,这个函数不供调用,仅做演示""" 55 | adb.server_version() # adb 版本 56 | adb.server_kill() # adb kill-server 57 | adb.connect("192.168.190.101:5555") 58 | 59 | def __adb_device(self): 60 | """AdbDevice() API,这个函数不供调用,仅做演示""" 61 | device = self.adb_device 62 | 63 | print(device.serial) # 获取设备串号 64 | 65 | print(device.prop.model) 66 | print(device.prop.device) 67 | print(device.prop.name) 68 | print(device.prop.get("ro.product.manufacturer")) 69 | 70 | # 获取当前应用包名和 activity 71 | # {'package': 'com.android.settings', 'activity': 'com.android.settings.MainSettings'} 72 | print(device.current_app()) 73 | 74 | # 安装 75 | device.install("apk_path...") 76 | # 卸载 77 | device.uninstall("com.esbook.reader") 78 | 79 | # 拉起应用 80 | device.app_start("com.android.settings") # 通过 monkey 拉起 81 | device.app_start("com.android.settings", "com.android.settings.MainSettings") # 通过 am start 拉起 82 | # 清空APP 83 | device.app_clear("com.esbook.reader") 84 | # 杀掉应用 85 | device.app_stop("com.esbook.reader") 86 | 87 | # 获取屏幕分辨率 1080*1920 88 | x, y = device.window_size() 89 | print(f"{x}*{y}") 90 | 91 | # 获取当前屏幕状态 亮/熄 92 | print(device.is_screen_on()) # bool 93 | 94 | device.switch_wifi(True) # 需要开启 root 权限 95 | device.switch_airplane(False) # 切换飞行模式 96 | device.switch_screen(True) # 亮/熄屏 97 | 98 | # 获取 IP 地址 99 | print(device.wlan_ip()) 100 | 101 | # 打开浏览器并跳转网页 102 | device.open_browser("http://www.baidu.com") 103 | 104 | device.swipe(500, 800, 500, 200, 0.5) 105 | device.click(500, 500) 106 | device.keyevent("HOME") # 发送事件 107 | device.send_keys("hello world$%^&*") # simulate: adb shell input text "hello%sworld\%\^\&\*" 108 | 109 | print(device.package_info("com.esbook.reader")) # dict 110 | 111 | print(device.rotation()) # 屏幕是否转向 0, 1, 2, 3 112 | device.screenrecord().close_and_pull() # 录屏 113 | 114 | device.list_packages() # 包名列表 ["com.example.hello"] 115 | 116 | @staticmethod 117 | def device_list(): 118 | try: 119 | return [d.serial for d in adb.device_list()] 120 | except RuntimeError: 121 | return [] 122 | 123 | def state(self): 124 | """获取设备状态 125 | Returns: 126 | Iterator[DeviceEvent], DeviceEvent.status can be one of 127 | ['device', 'offline', 'unauthorized', 'absent'] 128 | """ 129 | return adb.track_devices().__next__()[-1] 130 | 131 | def model(self) -> str: 132 | """获取设备型号: ELE-AL00""" 133 | return self.adb_device.prop.model 134 | 135 | def manufacturer(self) -> str: 136 | """获取设备制造商: HUAWEI""" 137 | return self.adb_device.prop.get("ro.product.manufacturer") 138 | 139 | def device_version(self) -> str: 140 | """获取设备 Android 版本号,如 4.2.2""" 141 | return self.adb_device.prop.get("ro.build.version.release") 142 | 143 | def sdk_version(self) -> str: 144 | """获取设备 SDK 版本号,如 26""" 145 | return self.adb_device.prop.get("ro.build.version.sdk") 146 | 147 | def cpu_version(self): 148 | """获取cpu基带版本: arm64-v8a""" 149 | return self.adb_device.prop.get("ro.product.cpu.abi") 150 | 151 | def wifi_state(self): 152 | """获取WiFi连接状态""" 153 | return 'enabled' in self.shell('dumpsys wifi | grep ^Wi-Fi') 154 | 155 | def wifi(self) -> str: 156 | """获取设备当前连接的 Wi-Fi 名称""" 157 | # output: mWifiInfo SSID: wifi_name, BSSID: 70:f9:6d:b6:a4:81, ... 158 | output = self.adb_device.shell("dumpsys wifi | grep mWifiInfo") 159 | name = output.strip().split(",")[0].split()[-1] 160 | return name 161 | 162 | def ip(self) -> str: 163 | """Get device IP 164 | 正常情况:inet addr:192.168.123.49 Bcast:192.168.123.255 Mask:255.255.255.0 165 | 无权限情况:ifconfig: ioctl 8927: Permission denied (Android 10) 166 | """ 167 | output = self.adb_device.shell("ifconfig wlan0") 168 | ip = re.findall(r'inet\s*addr:(.*?)\s', output, re.DOTALL) 169 | if not ip: 170 | return "获取失败" 171 | return ip[0] 172 | 173 | def connect(self, ip=None, port=5555) -> str: 174 | """基于 WI-FI 连接设备""" 175 | # TODO:判断手机与PC是否为同一网段,如果不是给出提示 176 | if not ip: 177 | ip = self.ip() 178 | if not ip: 179 | return '无线连接失败,请输入 IP 地址后重试!' 180 | 181 | # 设置端口 182 | dev = f"{ip.strip()}:{str(port).strip()}" 183 | logging.info(f"进行无线连接 ==> {dev}") 184 | self.shell(f'adb tcpip {port}') 185 | self.shell(f'adb connect {dev}') 186 | 187 | try: 188 | assert dev in self.device_list() 189 | return 'Successful wireless connection.' 190 | except AssertionError: 191 | return '无线连接失败,请确保手机和PC网络一致;或检查IP地址是否正确。' 192 | 193 | def open_proxy(self, hostname, port): 194 | """打开手机全局代理""" 195 | logging.info(f"打开全局代理 {hostname}:{port}") 196 | self.adb_device.shell(f"settings put global http_proxy {hostname}:{port}") 197 | 198 | def close_proxy(self): 199 | """关闭手机代理,不需要重启设备生效""" 200 | self.adb_device.shell(f"settings put global http_proxy :0") 201 | logging.info(f"关闭全局代理(亲测如不生效重启应用即可)") 202 | 203 | def delete_proxy(self): 204 | """删除手机代理,删除后需要重启设备""" 205 | # adb shell settings delete global global_http_proxy_host # 仅删除 hostname 206 | # adb shell settings delete global global_http_proxy_port # 仅删除 port 207 | self.shell(f"settings delete global http_proxy") 208 | 209 | def device_info_complete(self): 210 | try: 211 | ip = self.ip() 212 | except RuntimeError as e: 213 | ip = str(e).split(":")[-1] 214 | device_info_dict = { 215 | "【设备型号】": f"{self.manufacturer()} | {self.model()}", 216 | "【设备串号】": self.adb_device.serial, 217 | "【系统版本】": f"Android {self.device_version()} (API {self.sdk_version()})", 218 | "【电池状态】": self.battery_info(), 219 | "【CPU版本】": self.cpu_version(), 220 | "【屏幕尺寸】": f"{self.window_size()[0]}x{self.window_size()[-1]}", 221 | "【IP地址】\t": ip, 222 | } 223 | output = '' 224 | for k, v in device_info_dict.items(): 225 | output += f"{k}: {v}\n" 226 | return output 227 | 228 | def get_pid(self, pkg_name) -> str: 229 | """根据包名获取对应的 gid 230 | args: 231 | pkg_name -> 应用包名 232 | usage: 233 | get_pid("com.android.commands.monkey") # monkey 234 | """ 235 | pid_info = self.shell(f"adb -s {self.serial} shell ps | grep -w {pkg_name}")[0] 236 | if not pid_info: 237 | return "The process doesn't exist." 238 | pid = pid_info.split()[1] 239 | return pid 240 | 241 | def kill_pid(self, pid): 242 | """TODO:还没测试,杀死应用进程 monkey 进程 243 | usage: 244 | kill_pid(154) 245 | 246 | 注:杀死系统应用进程需要root权限 247 | """ 248 | print(self.adb_device.shell(f"kill {pid}")) 249 | # if self.shell("kill %s" % str(pid)).stdout.read().split(": ")[-1] == "": 250 | # return "kill success" 251 | # else: 252 | # return self.shell("kill %s" % str(pid)).stdout.read().split(": ")[-1] 253 | 254 | def pm_list_package(self, options="-3"): 255 | """输出所有软件包,可根据 options 进行过滤显示 256 | options: 257 | -s:进行过滤以仅显示系统软件包。 258 | -3:进行过滤以仅显示第三方软件包。 259 | -i:查看软件包的安装程序。 260 | """ 261 | return self.adb_device.shell(f"pm list packages {options}") 262 | 263 | def pm_path_package(self, package): 264 | """输出给定 package 的 APK 的路径""" 265 | return self.adb_device.shell(f"pm path {package}") 266 | 267 | def install(self, apk): 268 | """安装应用,支持本地路径安装和URL安装""" 269 | self.shell(f"python3 -m adbutils -s {self.serial} -i {apk}") 270 | 271 | def uninstall(self, package): 272 | """卸载应用""" 273 | self.adb_device.uninstall(package) 274 | 275 | def delete_folder(self, folder): 276 | """删除 sdcard 上的文件夹""" 277 | self.adb_device.shell(f"rm -rf /sdcard/{folder}") 278 | 279 | def battery_info(self) -> str: 280 | """获取电池信息 281 | returns: 282 | 100% (已充满, 29°) 283 | """ 284 | output = (self.adb_device.shell("dumpsys battery")) 285 | 286 | m = re.compile(r'level: (?P[\d.]+)').search(output) 287 | level = m.group("level") 288 | m = re.compile(r'status: (?P[\d.]+)').search(output) 289 | status = int(m.group("status")) 290 | m = re.compile(r'temperature: (?P[\d.]+)').search(output) 291 | temperature = int(m.group("temperature")) // 10 292 | 293 | # BATTERY_STATUS_UNKNOWN:未知状态 294 | # BATTERY_STATUS_CHARGING: 充电状态 295 | # BATTERY_STATUS_DISCHARGING: 放电状态 296 | # BATTERY_STATUS_NOT_CHARGING:未充电 297 | # BATTERY_STATUS_FULL: 充电已满 298 | status_dict = { 299 | 1: "未知状态", 300 | 2: "充电中", 301 | 3: "放电中", 302 | 4: "未充电", 303 | 5: "已充满" 304 | } 305 | 306 | battery_info = f"{level}% ({status_dict[status]}, {temperature}°)" 307 | return battery_info 308 | 309 | def window_size(self): 310 | """获取设备屏幕分辨率""" 311 | x, y = self.adb_device.window_size() 312 | return x, y 313 | 314 | def qr_code(self, text): 315 | """生成二维码, pip3 install MyQR""" 316 | # TODO:支持本地路径可以内网访问 ,地址为:http://192.168.125.81:8000/path 317 | qr_path = systemer.get_abs_path("log", "qr_code.jpg") 318 | logging.info(f"二维码路径:{qr_path}") 319 | myqr.run( 320 | words=text, # 不支持中文 321 | # pictures='2.jpg', # 生成带图的二维码 322 | # colorized=True, 323 | save_name=qr_path, 324 | ) 325 | return qr_path 326 | 327 | def reboot(self): 328 | """重启设备""" 329 | self.shell("adb reboot") 330 | 331 | def fast_boot(self): 332 | """进入fastboot模式""" 333 | self.shell("adb reboot bootloader") 334 | 335 | def current_app(self): 336 | """获取当前顶层应用的包名和activity,未命中系统应用,才返回值""" 337 | package_black_list = [ 338 | "com.miui.home", # 小米桌面 339 | "com.huawei.android.launcher" # 华为桌面 340 | ] 341 | current_app = self.adb_device.current_app() 342 | logging.debug(current_app) 343 | package = current_app["package"] 344 | activity = current_app["activity"] 345 | 346 | if package in package_black_list: 347 | logging.info(f"Current package is System APP ==> {package}") 348 | return 349 | return package, activity 350 | 351 | def current_app_reset(self): 352 | """重置当前应用""" 353 | pkg_act = self.current_app() 354 | if not pkg_act: 355 | return "当前为系统应用,请启动应用后重试" 356 | package, activity = pkg_act 357 | self.adb_device.app_clear(package) 358 | logging.info(f"Reset APP ==> {package}") 359 | self.adb_device.app_start(package_name=package) 360 | logging.info(f"Restart APP ==> {package}") 361 | 362 | def app_info(self, pkg_name): 363 | return self.adb_device.package_info(pkg_name) 364 | 365 | def current_app_info(self): 366 | """ 367 | return -> dict: 368 | version_name, 369 | version_code(去掉了最高和最低支持的 SDK 版本), 370 | flags, 371 | first_install_time, 372 | last_update_time, 373 | signature 374 | """ 375 | package, activity = self.current_app() 376 | package_info = self.adb_device.package_info(package) 377 | return package_info 378 | 379 | def call_phone(self, number: int): 380 | """启动拨号器拨打电话""" 381 | self.adb_device.shell(f"am start -a android.intent.action.CALL -d tel:{number}") 382 | 383 | def get_focused_package_xml(self, save_path): 384 | file_name = random.randint(10, 99) 385 | self.shell(f'uiautomator dump /data/local/tmp/{file_name}.xml').communicate() 386 | self.adb_device(f'pull /data/local/tmp/{file_name}.xml {save_path}').communicate() 387 | 388 | def click_by_percent(self, x, y): 389 | """通过比例发送触摸事件""" 390 | if 0.0 < x+y < 2.0: 391 | wx, wy = self.window_size() 392 | x *= wx 393 | y *= wy 394 | logging.debug(f"点击坐标 ==> ({x}, {y})") 395 | return self.adb_device.click(x, y) 396 | else: 397 | logging.error("click_by_percent(x, y) 预期为小于等于1.0的值,请检查参数") 398 | 399 | def swipe_by_percent(self, sx, sy, ex, ey, duration: float = 1.0): 400 | """通过比例发送滑动事件,Android 4.4以上可选 duration(ms)""" 401 | wx, wy = self.window_size() 402 | sx *= wx 403 | sy *= wy 404 | ex *= wx 405 | ey *= wy 406 | logging.debug(f"滑动事件 ==> ({sx}, {sy}, {ex}, {ey}, duration={duration})") 407 | return self.adb_device.swipe(sx, sy, ex, ey, duration=duration) 408 | 409 | def swipe_to_left(self): 410 | """左滑屏幕""" 411 | self.swipe_by_percent(0.8, 0.5, 0.2, 0.5) 412 | 413 | def swipe_to_right(self): 414 | """右滑屏幕""" 415 | self.swipe_by_percent(0.2, 0.5, 0.8, 0.5) 416 | 417 | def swipe_to_up(self): 418 | """上滑屏幕""" 419 | self.swipe_by_percent(0.5, 0.8, 0.5, 0.2) 420 | 421 | def swipe_to_down(self): 422 | """下滑屏幕""" 423 | self.swipe_by_percent(0.5, 0.2, 0.5, 0.8) 424 | 425 | def gen_file_name(self, suffix=None): 426 | """生成文件名称,用于给截图、日志命名""" 427 | now = datetime.datetime.now() 428 | str_time = now.strftime('%y%m%d_%H%M%S') 429 | device = self.manufacturer().lower() 430 | if not suffix: 431 | return f"{device}_{str_time}" 432 | return f"{device}_{str_time}.{suffix}" 433 | 434 | def screenshot(self, pc_path): 435 | """获取当前设备的截图,导出到指定目录 436 | usage: 437 | screenshot("/Users/lan/Downloads/") 438 | """ 439 | try: 440 | self.adb_device.shell("mkdir sdcard/screenshot") 441 | except FileExistsError: 442 | logging.debug("截图保存位置 ==> sdcard/screenshot/") 443 | 444 | filename = self.gen_file_name("png") 445 | file = f"/sdcard/screenshot/{filename}" 446 | self.adb_device.shell(f"/system/bin/screencap -p {file}") 447 | self.adb_device.sync.pull(file, f"{pc_path}/{filename}") 448 | 449 | def screen_record(self): 450 | pass 451 | 452 | def app_usage(self, package): 453 | """获取当前应用 cpu、内存使用占比 454 | """ 455 | # 安卓top命令仅显示16位包名,这里处理下方便 grep 456 | pkg = package[:15] + (package[15:] and "+") 457 | 458 | # -n 显示n次top的结果后命令就会退出 459 | # -d 更新间隔秒数 460 | # 各参数含义:https://blog.csdn.net/q1183345443/article/details/89920632 461 | output = self.adb_device.shell(f'top -n 1 -d 1 | grep {pkg}').split() 462 | logging.debug(f"返回数据 ==> {output}") 463 | 464 | cup, mem = output[8], output[9] 465 | return cup, mem 466 | 467 | def bigfile(self): 468 | """填充手机磁盘,直到满""" 469 | self.adb_device.shell('dd if=/dev/zero of=/mnt/sdcard/bigfile') 470 | 471 | def delete_bigfile(self): 472 | """删除填满磁盘的大文件""" 473 | self.adb_device.shell('rm -r /mnt/sdcard/bigfile') 474 | 475 | def backup_apk(self, package, path): 476 | """备份应用与数据(未测试) 477 | - all 备份所有 | -f 指定路径 | -system|-nosystem | -shared 备份sd卡 478 | """ 479 | self.adb_device.adb_output(f'backup -apk {package} -f {path}/mybackup.ab') 480 | 481 | def restore_apk(self, path): 482 | """恢复应用与数据(未测试)""" 483 | self.adb_device.adb_output('restore %s' % path) 484 | 485 | def logcat_c(self): 486 | self.adb_device.shell("logcat --clear") 487 | logging.info("logcat clear...") 488 | 489 | def logcat(self, filepath, timeout=5, flag=""): 490 | """获取 adb 日志 491 | Example: 492 | flag: '*:E';过滤错误级别日志 493 | """ 494 | command = f"{self.adb_path} -s {self.serial} logcat -v time {flag} > {filepath}" 495 | logging.info(f"==> {command}") 496 | output = subprocess.Popen(command, shell=True) 497 | pid = str(output.pid) 498 | sleep(timeout) 499 | logging.info(f"adb logcat finished... (PID: {pid}; time: {timeout}s)") 500 | 501 | system = platform.system() 502 | if system == "Darwin" or "Linux": 503 | output = subprocess.Popen(f"kill -9 {pid}", shell=True) 504 | else: 505 | # Windows 不知道能否生效,没有机器测试 506 | output = subprocess.Popen(f"taskkill /F /T /PID {pid}", shell=True) 507 | logging.info(f"Kill adb logcat process! ({system}:{output})") 508 | 509 | def dump_crash_log(self, filepath): 510 | """转储带有 crash pid 的日志 511 | filepath: 512 | 日志存储的目录路径 513 | """ 514 | # log 存储路径 515 | filename = self.gen_file_name() 516 | log_filename = f"{filename}.log" 517 | crash_filename = f"{filename}_crash.log" 518 | 519 | log_filepath = os.path.join(filepath, log_filename) 520 | crash_filepath = os.path.join(filepath, crash_filename) 521 | 522 | self.logcat(log_filepath, flag='*:E') 523 | logging.info(f"adb logcat *:E filepath ==> {log_filepath}") 524 | 525 | # 获取设备基础信息 526 | model = self.model() 527 | manufacturer = self.manufacturer() 528 | version = self.device_version() 529 | sdk_version = self.sdk_version() 530 | device_info = f"DeviceInfo: {manufacturer} {model} | Android {version} (API {sdk_version})" 531 | 532 | # 根据关键字找到出现 FATAL 错误的 pid 533 | keyword = "FATAL EXCEPTION: main" 534 | crash_pid_list = [] 535 | with open(log_filepath, encoding="utf-8") as fr: 536 | for line in fr.readlines(): 537 | if keyword in line: 538 | data = re.findall(r"\d+", line) # 提取出日志行内所有数字(日期 + PID) 539 | pid = data[-1] 540 | crash_pid_list.append(pid) 541 | 542 | logging.info(f"Crash PID list >>> {crash_pid_list}") 543 | 544 | # 根据 pid 过滤出错误日志,转储到新的文件内 545 | if crash_pid_list: 546 | with open(crash_filepath, "w+", encoding="utf-8") as f: # 创建转储日志并写入 547 | f.write(f"{'-' * 50}\n") 548 | f.write(f"{device_info}\n共出现 {len(crash_pid_list)} 次闪退\n") 549 | f.write(f"{'-' * 50}\n") 550 | with open(log_filepath, encoding="utf-8") as f1: # 读取原始日志 551 | for line in f1.readlines(): 552 | for pid in crash_pid_list: 553 | if pid in line: 554 | if "FATAL" in line: 555 | f.write("\n# begging of crash --- >>>\n") 556 | f.write(line) 557 | logging.info(f"Crash log path: {crash_filepath}") 558 | return crash_filename 559 | else: 560 | logging.info(f"Not found 'FATAL EXCEPTION: main' in {log_filepath}") 561 | return log_filename 562 | 563 | def find_process_id(self, pkg_name): 564 | """根据包名查询进程 PID 565 | :param pkg_name: Package Name 566 | :return: USER | PID | NAME 567 | """ 568 | output = self.adb_device.shell("ps | grep %s" % pkg_name) 569 | process_list = str(output[1]).split('\n') 570 | for process in process_list: 571 | if process.count(pkg_name): 572 | while process.count(' ') > 0: 573 | process = process.replace(' ', ' ') 574 | process_info = process.split(' ') 575 | user = process_info[0] 576 | pid = process_info[1] 577 | name = process_info[-1] 578 | return user, pid, name 579 | return None, None, None 580 | 581 | def find_and_kill_process(self, pkg_name): 582 | """查找并结束指定进程""" 583 | user, pid, process_name = self.find_process_id(pkg_name) 584 | if pid is None: 585 | return "None such process [ %s ]" % pkg_name 586 | if user == 'shell': 587 | kill_cmd = 'kill -9 %s' % pid 588 | else: 589 | kill_cmd = 'am force-stop %s' % pkg_name 590 | return self.shell(kill_cmd) 591 | 592 | 593 | if __name__ == '__main__': 594 | pass 595 | -------------------------------------------------------------------------------- /utils/common.py: -------------------------------------------------------------------------------- 1 | """ 2 | @Time : 2020/6/18 11:15 上午 3 | @Author : lan 4 | @Mail : lanzy.nice@gmail.com 5 | @Desc : 6 | """ 7 | import os 8 | import re 9 | import sys 10 | import time 11 | from threading import Thread 12 | from xml.dom import minidom 13 | 14 | import yaml 15 | import ctypes 16 | import socket 17 | import logging 18 | import inspect 19 | import datetime 20 | import linecache 21 | 22 | from MyQR import myqr 23 | 24 | from config import config 25 | from utils import systemer 26 | 27 | 28 | class MyThread(Thread): 29 | 30 | def __init__(self,func,args=()): 31 | super(MyThread,self).__init__() 32 | self.func = func 33 | self.args = args 34 | 35 | def run(self): 36 | self.result = self.func(*self.args) 37 | 38 | def get_result(self): 39 | try: 40 | # 如果子线程不使用join方法,此处可能会报没有self.result的错误 41 | return self.result 42 | except Exception: 43 | return None 44 | 45 | 46 | def get_pc_ip(): 47 | """获取电脑的 IP 地址""" 48 | try: 49 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 50 | s.connect(('8.8.8.8', 80)) 51 | ip = s.getsockname()[0] 52 | finally: 53 | s.close() 54 | return ip 55 | 56 | 57 | def sleep(secs): 58 | return time.sleep(secs) 59 | 60 | 61 | def get_current_time(t=0): 62 | """ 63 | 四种输出格式:t=0 默认输出格式:2016-07-19 18:54:18.282000 64 | t=3 返回当前日期:2018-04-01 65 | :param t: 入参 66 | :return: 按格式返回当前系统时间戳 67 | """ 68 | curr_time = datetime.datetime.now() 69 | if t == 0: 70 | return curr_time # 格式:2016-07-19 18:54:18.282000 71 | elif t == 1: 72 | return curr_time.strftime('%Y-%m-%d %H:%M:%S') # 格式:2016-07-19 18:11:04 73 | elif t == 2: 74 | return curr_time.strftime('%Y%m%d-%H%M%S') # 格式:20160719-181104 75 | elif t == 3: 76 | return curr_time.strftime('%Y-%m-%d') # 格式:2016-07-19 77 | else: 78 | print("[warning]: no format matches...pls check!") 79 | 80 | 81 | def time_diff(start_time, stop_time): 82 | """ 83 | 求时间差用datetime模块,不能用time()模块,且不能使用格式化的输出 84 | start_time和stop_time需datetime.datetime.now()获取 85 | """ 86 | t = (stop_time - start_time) 87 | time_day = t.days 88 | s_time = t.seconds 89 | ms_time = t.microseconds / 1000000 90 | used_time = int(s_time + ms_time) 91 | time_hour = used_time / 60 / 60 92 | time_minute = (used_time - time_hour * 3600) / 60 93 | time_second = used_time - time_hour * 3600 - time_minute * 60 94 | time_microsecond = (t.microseconds - t.microseconds / 1000000) / 1000 95 | ret_str = "%d天%d小时%d分%d秒%d毫秒" % (time_day, time_hour, time_minute, time_second, time_microsecond) 96 | return ret_str 97 | 98 | 99 | def create_timestamp_folder(path='', t=3): 100 | """ 101 | create timestamp folder and return the folder name 102 | :param path: 默认时,在当前目录创建 103 | :param t: 104 | :return: folder name 105 | """ 106 | folder_name = path + get_current_time(t) 107 | # check file is exists or not. 108 | try: 109 | if not os.path.isdir(folder_name): 110 | os.mkdir(folder_name) 111 | except Exception as e: 112 | print(str(e) + " Error : Failed to create folder...") 113 | return folder_name 114 | 115 | 116 | def gen_file_name(before_name=None, suffix=None): 117 | """生成文件名称,用于给截图、日志命名""" 118 | now = datetime.datetime.now() 119 | str_time = now.strftime('%y%m%d_%H%M%S') 120 | before = before_name.lower() 121 | if not suffix: 122 | return f"{before}_{str_time}" 123 | return f"{before}_{str_time}.{suffix}" 124 | 125 | 126 | def parse_yml(file_path): 127 | """解析给定路径的yml文件并返回内容""" 128 | f = open(file_path) 129 | yam_content = yaml.load(f) 130 | f.close() 131 | return yam_content 132 | 133 | 134 | def get_yml_value(file_path, section): 135 | """ 136 | 解析给定路径的yml文件并返回内容具体选择区域的section列表数据 137 | """ 138 | f = open(file_path) 139 | yml_value = yaml.load(f)[section] 140 | f.close() 141 | return yml_value 142 | 143 | 144 | def open_xml_file(file_name, first_node, second_node): 145 | """ 读取xml文件 """ 146 | # 使用minidom打开文档 147 | # 从内存空间为该文件申请内存 148 | xml_file = minidom.parse("../config/" + file_name) 149 | # 一级标签(标签可重复,加角标区分) 150 | one_node = xml_file.getElementsByTagName(first_node)[0] 151 | # 二级标签 152 | two_node = one_node.getElementsByTagName(second_node)[0].childNodes[0].nodeValues 153 | return two_node 154 | 155 | 156 | def __line__(file=''): 157 | """获取调用处的文件名称,代码行数 158 | ex: 159 | call method: __line__(__file__) 160 | """ 161 | try: 162 | raise Exception 163 | except: 164 | f = sys.exc_info()[2].tb_frame.f_back 165 | func_name = str(f.f_code.co_name)+'()' 166 | line = str(f.f_lineno) 167 | if file == '': 168 | return "[%s | %s]" % (func_name, line) 169 | else: 170 | return "[%s | %s | %s]" % (file, func_name, line) 171 | 172 | 173 | def modify_config(keyword: str, expected: str): 174 | """ 175 | 修改配置文件中关键字所在行的内容 176 | :param keyword: 查找配置文件的关键字 177 | :param expected: expected result 想替换的预期字符串(替换行) 178 | :return: 179 | """ 180 | # 添加以下代码目的是: 将当前项目目录临时添加到环境变量 181 | cur_path = os.path.abspath(os.path.dirname(__file__)) 182 | root_path = os.path.split(cur_path)[0] 183 | sys.path.append(root_path) 184 | with open('./config/config.py', 'r+') as f, open('./config/config1.py', 'w') as fw: 185 | for line in f: 186 | if keyword in line: 187 | line = '%s\n' % expected 188 | fw.write(line) 189 | os.remove('./config/config.py') 190 | os.rename('./config/config1.py', './config/config.py') 191 | 192 | 193 | def _async_raise(tid, exc_type): 194 | """raises the exception, performs cleanup if needed""" 195 | tid = ctypes.c_long(tid) 196 | if not inspect.isclass(exc_type): 197 | exc_type = type(exc_type) 198 | res = ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, ctypes.py_object(exc_type)) 199 | if res == 0: 200 | raise ValueError("invalid thread id") 201 | elif res != 1: 202 | # """if it returns a number greater than one, you're in trouble, 203 | # and you should call it again with exc=NULL to revert the effect""" 204 | ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, None) 205 | raise SystemError("PyThreadState_SetAsyncExc failed") 206 | 207 | 208 | def stop_thread(thread): 209 | """杀死子线程""" 210 | _async_raise(thread.ident, SystemExit) 211 | 212 | 213 | def get_line_context(file_path, line_num): 214 | """读取文件某行""" 215 | return linecache.getline(file_path, line_num).strip() 216 | 217 | 218 | def perf_timer(func): 219 | def call_func(*args, **kwargs): 220 | start_time = time.time() 221 | func(*args, **kwargs) 222 | end_time = time.time() 223 | total_time = end_time - start_time 224 | logging.info( 225 | f"[{func.__name__}] Time elapsed:{int(total_time // 60)}min & {total_time % 60:.2f}s!" 226 | ) 227 | return call_func 228 | 229 | 230 | def qr_code(text): 231 | """生成二维码, pip3 install MyQR""" 232 | qr_path = systemer.get_abs_path("log", "qr_code.jpg") 233 | logging.info(f"二维码路径:{qr_path}") 234 | myqr.run( 235 | words=text, # 不支持中文 236 | # pictures='2.jpg', # 生成带图的二维码 237 | # colorized=True, 238 | save_name=qr_path, 239 | ) 240 | return qr_path 241 | 242 | 243 | if __name__ == "__main__": 244 | pass 245 | # user_info = get_yml_value('../config/data.yml', 'phone_login') 246 | # print(user_info) 247 | # print(user_info[0]['phone1']) 248 | # print(create_timestamp_folder()) 249 | # print("oscar test001") 250 | # __line__(__file__) 251 | get_pc_ip() 252 | 253 | 254 | 255 | 256 | -------------------------------------------------------------------------------- /utils/fastbot_android.py: -------------------------------------------------------------------------------- 1 | """ 2 | @lan 3 | 4 | # 基础配置 5 | max.config 6 | 7 | max.schema 8 | max.strings 随机输入字符串配置 9 | max.path.actions 自定义事件序列;场景覆盖不全,通过人工配置到达fastbot遍历不到的场景 10 | max.widget.black 屏蔽控件或区域 11 | max.tree.pruning 树剪枝屏蔽控件;效率更高,通常与黑控件同时屏蔽 12 | 13 | # 黑白名单不能同时配置,非黑即白 14 | awl.strings 白名单配置 15 | abl.strings 黑名单配置 16 | 17 | TODO: 与 u2 冲突,都需要 uiautomator 服务,启停 u2 的功能,后续看是否需要添加 18 | """ 19 | import os 20 | import logging 21 | 22 | from adbutils import adb 23 | 24 | from config import config 25 | from utils import systemer 26 | from utils import logger 27 | 28 | 29 | class FastbotAndroid: 30 | 31 | def __init__(self, package, duration, serial=None, throttle=300, output="fastbot"): 32 | 33 | self.pkg_name = f"-p {package}" if package else None # 遍历APP的包名 34 | self.duration = f"--running-minutes {duration}" if duration else None # 遍历时长,单位:分钟 35 | self.throttle = f"--throttle {throttle}" if throttle else None # 遍历事件频率,建议为500-800,单位:毫秒 36 | self.output = output 37 | self.output_dir = f"--output-directory /sdcard/{output}" if output else None # log/crash 另存目录 38 | # /sdcard/crash-dump.log # crash default path 39 | # /sdcard/oom-traces.log # OOM default path 40 | 41 | self.awl = f"--act-whitelist-file /sdcard/awl.strings" 42 | 43 | self.adb = adb.device(serial=serial) # 多个设备需要指定设备号 44 | self.adb.shell(f"rm -rf /sdcard/{output}") 45 | 46 | def check_rely(self): 47 | # 推送所有依赖到设备 48 | jar = ["monkeyq.jar", "framework.jar"] 49 | conf = [ 50 | "max.config", 51 | "max.strings", # 随机输入字符配置 52 | "awl.strings", # 白名单配置 --act-whitelist-file 53 | # "abl.strings", # 黑名单配置 --act-blacklist-file 54 | "max.widget.black", 55 | "max.tree.pruning", 56 | "max.xpath.actions", 57 | ] 58 | 59 | jar_path = config.FASTBOT_PATH 60 | 61 | for i in jar: 62 | name = self.adb.shell(f"ls sdcard | grep '{i}'") 63 | if not name: 64 | self.adb.push(f"{jar_path}/{i}", "/sdcard/") 65 | logging.info(f"adb push >>> sdcard/{i}") 66 | for i in conf: 67 | self.adb.push(f"{jar_path}/{i}", "/sdcard/") 68 | logging.info(f"adb push >>> sdcard/{i}") 69 | 70 | # 清空设备日志;缓冲区自动清理,不清理也行 71 | # self.adb.shell("logcat --clear") 72 | 73 | def set_keyboard(self, name="ADBKeyboard"): 74 | """自定义输入法 + 屏蔽输入栏""" 75 | if name == "ADBKeyboard": 76 | if "com.android.adbkeyboard" not in self.adb.list_packages(): 77 | apk = os.path.join(config.FASTBOT_PATH, "ADBKeyBoard.apk") 78 | self.adb.install(apk) 79 | self.adb.shell("ime set com.android.adbkeyboard/.AdbIME") # 设置为 adbKeyboard 输入法 80 | else: 81 | # TODO: 这里的输入法是华为的,可能不兼容其他厂商,先这样吧 82 | self.adb.shell("ime set com.baidu.input_huawei/.ImeService") # 设置为百度输入法 83 | 84 | def exec_fastbot(self): 85 | # 执行入口(固定不变) 86 | jar = "CLASSPATH=/sdcard/monkeyq.jar:/sdcard/framework.jar" # jar 包路径 87 | exec = "exec app_process /system/bin com.android.commands.monkey.Monkey" # 执行入口 88 | mode = "--agent robot" # 遍历模式,无需更改 89 | bug_report = "--bugreport" # 崩溃时保存 bug report log 90 | log_level = "-v -v -v" 91 | 92 | """ 93 | monkey 参数: 94 | https://developer.android.com/studio/test/monkey 95 | https://www.cnblogs.com/sunzzc/p/13185573.html 96 | 97 | # 下面是 maxim 参数 98 | --pct-rotation 0 # 取消旋转屏幕;在这里设置后依然见过出现屏幕旋转情况 99 | --pct-back 5 # 设置 BACK 占比,默认占比 10% 100 | --pct-touch 100 # 设置点击比例 101 | --pct-reset 0 # fastbot不支持,别设置,会报错 102 | """ 103 | monkey_params = "--pct-rotation 0 --pct-motion 50" 104 | 105 | cmd = f"{jar} {exec} {self.pkg_name} {mode} {monkey_params} " \ 106 | f"{self.awl} {self.duration} {self.throttle} {log_level} {self.output_dir}" 107 | 108 | # utils shell 109 | systemer.shell(f"adb shell {cmd}") 110 | 111 | def log_dump(self): 112 | """如果出现闪退,则将日志拉到 PC""" 113 | crash = self.adb.shell(f"ls sdcard/{self.output} | grep -i 'crash'") 114 | oom = self.adb.shell(f"ls sdcard/{self.output} | grep -i 'oom'") 115 | 116 | if crash: 117 | logging.info(f"Crash log: sdcard/{crash}") 118 | # self.adb.sync.pull(crash, config.LOGCAT_PATH) 119 | logging.info(f"已拉取: {config.LOGCAT_PATH}") 120 | if oom: 121 | logging.info(f"Oom log: sdcard/{oom}") 122 | # self.adb.sync.pull(oom, config.LOGCAT_PATH) 123 | logging.info(f"已拉取: {config.LOGCAT_PATH}") 124 | 125 | def run(self): 126 | # 检测运行环境 127 | self.check_rely() 128 | # 设置adbKeyboard 129 | self.set_keyboard() 130 | # 执行 fastbot 131 | self.exec_fastbot() 132 | # 拉取日志 133 | self.log_dump() 134 | 135 | 136 | if __name__ == '__main__': 137 | logger.Logger() 138 | fastbot = FastbotAndroid(package="com.esbook.reader", duration=1) 139 | fastbot.run() 140 | 141 | -------------------------------------------------------------------------------- /utils/logger.py: -------------------------------------------------------------------------------- 1 | """ 2 | @Time : 2020/8/6 下午12:37 3 | @Author : lan 4 | @Mail : lanzy.nice@gmail.com 5 | @Desc : logging 模块初始化 6 | """ 7 | 8 | import os 9 | import time 10 | import logging 11 | import colorlog 12 | from logging.handlers import RotatingFileHandler 13 | 14 | 15 | class Logger: 16 | """初始化日志记录器 17 | usage: 18 | # 默认记录器是 root,会输出到控制台且写入到日志文件 output.log 19 | >>> logger1 = Logger().logger 20 | >>> logger1.debug("this is debug message") 21 | >>> import logging 22 | >>> logging.warning("this is warning message") 23 | 24 | # 创建记录器 test,会输出到控制台且写入到日志文件 test.log 25 | # 可以设置 level 级别 26 | >>> logger2 = Logger("test", level=logging.INFO).logger 27 | >>> logger2.error("this is error message") 28 | 29 | # 仅在控制台输出 30 | >>> logger3 = Logger(Logger.CONSOLE).logger 31 | >>> logger3.info("this is info message") 32 | """ 33 | CONSOLE = "console" 34 | 35 | __date_fmt = "%y%m%d %H:%M:%S" 36 | __fmt = "[ %(levelname)1.1s %(asctime)s %(module)s:%(lineno)d ] %(message)s" 37 | __color_fmt = '%(log_color)s[ %(levelname)1.1s %(asctime)s %(module)s:%(lineno)d ]%(black)s %(message)s' 38 | __filepath = os.path.dirname(os.path.dirname(__file__)) + "/" 39 | 40 | def __init__(self, filename=None, level=logging.INFO): 41 | """初始化 Logger""" 42 | 43 | """ 44 | 1. 创建记录器 45 | """ 46 | self.logger = logging.getLogger(filename) # 默认 ROOT 47 | self.logger.setLevel(level) 48 | 49 | """ 50 | 2. 定义输出格式 51 | """ 52 | self.__colors_config = { 53 | 'DEBUG': 'cyan', 54 | 'INFO': 'blue', 55 | 'WARNING': 'yellow', 56 | 'ERROR': 'red', 57 | 'CRITICAL': 'bold_red', 58 | } 59 | 60 | self.__datetime_fmt = "%y%m%d %H:%M:%S" 61 | self.__fmt_begin = "[ %(levelname)1.1s %(asctime)s %(module)s:%(lineno)d ]" 62 | self.__fmt_message = "%(message)s" 63 | self.__log_fmt = f"{self.__fmt_begin} {self.__fmt_message}" 64 | self.__log_colors_fmt = f'%(log_color)s{self.__fmt_begin}%(black)s {self.__fmt_message}' 65 | 66 | formatter = logging.Formatter( 67 | fmt=self.__log_fmt, 68 | datefmt=self.__datetime_fmt 69 | ) 70 | color_formatter = colorlog.ColoredFormatter( 71 | fmt=self.__color_fmt, 72 | datefmt=self.__datetime_fmt, 73 | log_colors=self.__colors_config 74 | ) 75 | 76 | """ 77 | 3. 创建处理器并关联输出格式 78 | # 如果没有给处理器指定日志级别,将使用记录器的日志级别 79 | # 如果没有给记录器指定日志级别,那么会使用默认「warning」级别 80 | # 如果两者都设置了指定日志级别,那么以记录器的级别为准 81 | """ 82 | # 3.1 创建 控制台输出 处理器 83 | console_handler = logging.StreamHandler() 84 | console_handler.setLevel(logging.DEBUG) 85 | console_handler.setFormatter(color_formatter) 86 | 87 | # 3.2 创建 文件输出 处理器 88 | __log_abs_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "log") 89 | day_time = time.strftime('%Y-%m-%d', time.localtime(time.time())) # 2020-04-20 90 | __filename = f"{day_time}.log" if not filename or filename == Logger.CONSOLE \ 91 | else f"{filename}_{day_time}.log" 92 | __file_path = os.path.join(__log_abs_path, __filename) 93 | if not os.path.exists(__log_abs_path): 94 | os.mkdir(__log_abs_path) 95 | 96 | # file_handler = logging.FileHandler(filename=__file_path) 97 | file_handler = RotatingFileHandler( 98 | filename=__file_path, 99 | mode="a+", 100 | maxBytes=1024*1024*5, # 5M 101 | backupCount=5, 102 | encoding="utf-8" 103 | ) 104 | file_handler.setLevel(logging.DEBUG) 105 | file_handler.setFormatter(formatter) 106 | 107 | """ 108 | 4. 记录器关联处理器 109 | """ 110 | if filename == "console": 111 | self.logger.addHandler(console_handler) 112 | else: 113 | self.logger.addHandler(console_handler) 114 | self.logger.addHandler(file_handler) 115 | 116 | """ 117 | 5. 创建 & 关联 过滤器 118 | # 仅输出设置的记录器下的日志,也可以对处理器进行设置过滤器 119 | # 这里设置为 test,就不会再输出日志了,因为只输出名为 "test" 记录器的日志 120 | """ 121 | # filter_app = logging.Filter("test") 122 | # self.logger.addFil(filter_app) 123 | 124 | 125 | Logger(level=logging.DEBUG) 126 | 127 | 128 | if __name__ == '__main__': 129 | Logger() 130 | # 不会输出 DEBUG 日志,默认从 INFO 开始 131 | logging.debug("this is debug message") 132 | logging.info("this is info message") 133 | logging.warning("this is warning message") 134 | logging.error("this is error message") 135 | logging.critical("this is critical message") 136 | 137 | # 会打印出 debug 日志 138 | # Logger(level=logging.DEBUG) 139 | # logging.debug("this is debug message") 140 | 141 | # 创建新的记录器,输出日志到 output1.log 内 142 | # logging = Logger("output1").logger 143 | # logging.info("this is test output1 log") 144 | 145 | 146 | -------------------------------------------------------------------------------- /utils/systemer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | """ 3 | Created on 2019-08-17 4 | """ 5 | import os 6 | import socket 7 | import logging 8 | import platform 9 | import subprocess 10 | 11 | 12 | def get_system() -> str: 13 | """获取当前系统名称 14 | Darwin | Windows | Linux 15 | """ 16 | return platform.system() 17 | 18 | 19 | def get_host_ip(): 20 | """查询本机(PC) IP 地址 21 | """ 22 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 23 | try: 24 | s.connect(('8.8.8.8', 80)) 25 | ip = s.getsockname()[0] 26 | finally: 27 | s.close() 28 | return ip 29 | 30 | 31 | def get_find_str() -> str: 32 | """根据系统类型选择 过滤 命令 33 | """ 34 | system = get_system() 35 | return 'findstr' if system == 'Windows' else 'grep' 36 | 37 | 38 | def shell(command, exec_code=False): 39 | """exec shell command. 40 | usage: 41 | >>> shell("ping www.baidu.com") # doctest: +SKIP 42 | ... 43 | >>> shell("echo 'hello'", exec_code=True) 44 | (0, ['hello']) 45 | """ 46 | if not isinstance(command, str): 47 | raise TypeError("command args type invalid", type(command)) 48 | 49 | proc = subprocess.Popen( 50 | command, 51 | stdin=subprocess.PIPE, 52 | stdout=subprocess.PIPE, 53 | shell=True, 54 | encoding="utf-8" 55 | ) 56 | 57 | proc.stdin.write(command) 58 | proc.stdin.flush() 59 | proc.stdin.close() 60 | 61 | logging.info(f"[ shell ] {command}") 62 | 63 | # Real time stdout of subprocess 64 | stdout = [] 65 | while True: 66 | line = proc.stdout.readline().strip() 67 | if line == "" and proc.poll() is not None: 68 | break 69 | stdout.append(line) 70 | logging.info(line) 71 | 72 | # Wait for child process and get return code 73 | # 0: 正常结束; 1: sleep; 2: 子进程不存在; -1/5: kill; None: 正在运行 74 | return_code = proc.wait() 75 | if not exec_code: 76 | return stdout 77 | return return_code, stdout 78 | 79 | 80 | def get_abs_path(*args): 81 | """get current project abs path. 82 | usage: 83 | >>> get_abs_path() 84 | /Users/lan/workspace/pythonProject/toolkit 85 | >>> get_abs_path("log") 86 | /Users/lan/workspace/pythonProject/toolkit/log 87 | """ 88 | abs_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 89 | if not args: 90 | return abs_path 91 | for path in args: 92 | abs_path = os.path.join(abs_path, path) 93 | return abs_path 94 | 95 | 96 | def mkdir(dir_path): 97 | """创建目录""" 98 | # 去除首位空格 99 | _dir = dir_path.strip().rstrip("\\").rstrip("/") 100 | logging.info(f"初始化目录: {dir_path}") 101 | 102 | # 判断路径是否存在 103 | is_exists = os.path.exists(_dir) 104 | 105 | if not is_exists: 106 | try: 107 | os.makedirs(_dir) 108 | logging.info("Directory creation success:%s" % _dir) 109 | except Exception as e: 110 | logging.error("Directory creation failed:%s" % e) 111 | else: 112 | # 如果目录存在则不创建,并提示目录已存在 113 | logging.info("Directory already exists:%s" % str(_dir)) 114 | 115 | 116 | def touch_file(file_path, content): 117 | """创建文件并写入内容,当文件存在时 118 | """ 119 | if os.path.exists(file_path): 120 | logging.info("{} is exists!".format(file_path)) 121 | return 122 | with open(file_path, "w", encoding="utf-8") as f: 123 | f.write(content) 124 | 125 | 126 | def get_latest_file(dir): 127 | """获取当前目录下最新的文件""" 128 | file_list = os.listdir(dir) 129 | 130 | if not file_list: 131 | return 132 | 133 | file_list.sort( 134 | key=lambda fn: os.path.getmtime(os.path.join(dir, fn)) 135 | if not os.path.isdir(os.path.join(dir, fn)) else 0 136 | ) 137 | filename = os.path.join(dir, file_list[-1]) 138 | return filename 139 | 140 | def open_path(dir): 141 | """打开当前目录""" 142 | if not dir: 143 | return False 144 | 145 | if get_system() == "Windows": 146 | cmd = f"start explorer {dir}" 147 | else: 148 | cmd = f"open {dir}" 149 | 150 | shell(cmd) 151 | return True 152 | 153 | 154 | if __name__ == '__main__': 155 | # print(get_abs_path()) 156 | pass 157 | -------------------------------------------------------------------------------- /utils/tidevice.py: -------------------------------------------------------------------------------- 1 | """ 2 | 使用 tidevice 与 ios 设备进行交互 3 | """ 4 | 5 | import logging 6 | import platform 7 | import subprocess 8 | import time 9 | import tidevice 10 | import threading 11 | 12 | from config import config 13 | from utils import systemer, common 14 | from utils.common import perf_timer, gen_file_name 15 | from utils.logger import Logger 16 | 17 | 18 | class TiDevice: 19 | 20 | def __init__(self, device: str = None): 21 | self.device = f"-u {device}" if device else "" 22 | 23 | def shell(self, args, exec_code=False): 24 | cmd = f"tidevice {self.device} {args}" 25 | return systemer.shell(cmd, exec_code=exec_code) 26 | 27 | def version(self): 28 | """show current version 29 | :return 30 | tidevice version 0.1.11 31 | """ 32 | return self.shell("version") 33 | 34 | def list(self, name=False): 35 | """show connected iOS devices 36 | :return 37 | List of apple devices attached 38 | 7017c3493f7a50f2c90a8ec56f1556b92089732c iPhone 39 | """ 40 | udid_list = [] 41 | name_list = [] 42 | output = self.shell("list") 43 | for device in output: 44 | device = device.split(" ", 1) 45 | udid_list.append(device[0]) 46 | name_list.append(device[-1]) 47 | if name: 48 | return name_list 49 | else: 50 | return udid_list 51 | 52 | def info(self): 53 | """show device info 54 | """ 55 | return self.shell("info") 56 | 57 | def sysinfo(self): 58 | """show device system info (json) 59 | """ 60 | return self.shell("sysinfo") 61 | 62 | @perf_timer 63 | def install(self, path): 64 | """install application 65 | """ 66 | return self.shell(f"install {path}", exec_code=True) 67 | 68 | def uninstall(self, pkg_name): 69 | """uninstall application 70 | """ 71 | return self.shell(f"uninstall {pkg_name}") 72 | 73 | def launch(self, pkg_name): 74 | return self.shell(f"launch {pkg_name}") 75 | 76 | def kill(self, pkg_name): 77 | return self.shell(f"kill {pkg_name}") 78 | 79 | def app_list(self): 80 | """获取该已安装 APP 列表 81 | """ 82 | return self.shell("applist") 83 | 84 | def screenshot(self): 85 | """take screenshot 86 | """ 87 | self.shell(f"screenshot") 88 | 89 | if len(self.list(name=True)) > 0: 90 | device = self.list(name=True)[0] 91 | else: 92 | device = "iphone" 93 | device = device.replace(" ", "") 94 | filename = gen_file_name(before_name=device, suffix="jpg") 95 | filepath = f"{config.SCREEN_PATH}/{filename}" 96 | 97 | output = systemer.shell( 98 | f"mv {config.get_abs_path()}/screenshot.jpg {filepath}", 99 | exec_code=True 100 | ) 101 | if output[0] == 0: 102 | logging.info(f"截图保存路径:{filepath}") 103 | return filepath 104 | else: 105 | logging.error("截图失败") 106 | return 107 | 108 | def reboot(self): 109 | """reboot device 110 | """ 111 | self.shell("reboot") 112 | 113 | def parse(self, uri): 114 | """parse ipa bundle id 115 | usage: 116 | tidevice parse [-h] uri 117 | """ 118 | self.shell(f"parse -h {uri}") 119 | 120 | def watch(self): 121 | """watch device 监听设备连接 122 | """ 123 | self.shell("watch") 124 | 125 | def wait_for_device(self): 126 | """wait for device attached 127 | """ 128 | self.shell("wait-for-device") 129 | 130 | def xctest(self, args): 131 | """run XCTest 132 | usage: 133 | # 修改监听端口为8200 134 | xctest("-B com.facebook.wda.WebDriverAgent.Runner -e USB_PORT:8200") 135 | """ 136 | self.shell(f"xctest {args}") 137 | 138 | def fastbot(self, bundle_id, duration, throttle=300, debug=False): 139 | """执行 fastbot 测试 140 | args: 141 | BUNDLEID: 包名 142 | dataPort: 143 | launchenv: 144 | duration: 执行时间 145 | throttle: 间隔时间 146 | """ 147 | fast_runner = "-B bytedance.FastbotRunner.lan.xctrunner" 148 | _debug = "--debug" if debug else "" 149 | _bundle_id = f"-e BUNDLEID:{bundle_id}" 150 | _duration = f"-e duration:{duration}" 151 | _throttle = f"-e throttle:{throttle}" 152 | # tidevice xctest -B bytedance.FastbotRunner.lan.xctrunner -e BUNDLEID:com.easou.esbook -e duration:3 153 | return self.xctest(f"{fast_runner} {_debug} {_bundle_id} {_duration} {_throttle}") 154 | 155 | def logcat(self, bundle_id, duration=10): 156 | """根据 bundle id 过滤日志""" 157 | duration = int(duration) * 60 158 | 159 | grep = systemer.get_find_str() 160 | 161 | phone = self.shell("info")[0].split(":")[-1].strip().replace(" ", "_") 162 | name = common.gen_file_name(before_name=phone, suffix="log") 163 | log_path = f"{config.LOGCAT_PATH}/{name}" 164 | 165 | command = f"tidevice syslog > {log_path}" 166 | logging.info(f"tidevice log command ==> {command}") 167 | output = subprocess.Popen(command, shell=True) 168 | 169 | pid = str(output.pid) 170 | logging.info(f"tidevice log pid ==> {pid}") 171 | 172 | time.sleep(int(duration)) 173 | logging.info(f"adb logcat finished... ({duration}s)") 174 | 175 | # 杀掉 logcat 进程 176 | system = platform.system() 177 | if system == "Darwin" or "Linux": 178 | subprocess.Popen(f"kill -9 {pid}", shell=True) 179 | logging.info(f"Kill tidevice logcat process! ({system})") 180 | else: 181 | # Windows 不知道能否生效,没有机器测试 182 | subprocess.Popen(f"taskkill /F /T /PID {pid}", shell=True) 183 | return log_path 184 | 185 | def fastbot_and_logcat(self, **kwargs): 186 | """执行fastbot命令,同时输出日志""" 187 | bundle = kwargs["bundle_id"] 188 | duration = kwargs["duration"] 189 | throttle = kwargs["throttle"] if "throttle" in kwargs.keys() else 300 190 | 191 | fastbot = threading.Thread(target=self.fastbot, args=(bundle, duration, throttle)) 192 | logcat = threading.Thread(target=self.logcat, args=(bundle, duration)) 193 | 194 | fastbot.start() 195 | logcat.start() 196 | 197 | fastbot.join() 198 | logging.info("fastbot thread has ended!") 199 | 200 | logcat.join() 201 | logging.info("tidevice log thread has ended!") 202 | 203 | logging.info("fastbot_and_logcat() function has ended!") 204 | 205 | def performance(self): 206 | # TODO: 暂不可用 207 | # 命令行执行:tidevice perf -B com.lan.fishing 208 | t = tidevice.Device() 209 | perf = tidevice.Performance(t) 210 | 211 | def callback(_type: tidevice.DataType, value: dict): 212 | print("R:", _type.value, value) 213 | 214 | perf.start("com.easou.esbook", callback=callback) 215 | time.sleep(10) 216 | perf.stop() 217 | 218 | if __name__ == '__main__': 219 | Logger() 220 | d = TiDevice() 221 | # d.fastbot(bundle_id="com.lan.fishing", duration=1) 222 | # d.fastbot_and_logcat(bundle_id="com.easou.esbook", duration=1) 223 | 224 | d.screenshot() 225 | 226 | 227 | 228 | --------------------------------------------------------------------------------