├── .idea └── vcs.xml ├── README.md ├── Results_ZIP └── iOS_20181226152906.zip ├── iOSCrashAnalysis ├── BaseIosCrash.py ├── BaseIosPhone.py ├── CrashExport.py ├── FileOperate.py ├── __pycache__ │ ├── BaseIosPhone.cpython-36.pyc │ ├── CrashExport.cpython-36.pyc │ ├── FileOperate.cpython-36.pyc │ ├── getPakeage.cpython-36.pyc │ ├── mysql_monkey.cpython-36.pyc │ └── mysql_operation.cpython-36.pyc ├── crash_mail.py ├── getPakeage.py ├── mysql_monkey.py └── symbolicatecrash ├── iOSMonkey.pptx └── monkey.py /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 1228 新增内容 2 | a.自动获取真机设备名及duid 3 | b.iOS crashreport解析优化及测试结果DB存储 4 | c.测试失败邮件通知 5 | d.Web页面结果展示及支持crash log下载 6 | 7 | ## 1.环境 8 | Mac mini:10.12.6 9 | xcode:9.2 10 | python:python3.6 11 | 12 | ## 2.备注 13 | a.FastMonkey相关问题参照@zhangzhao_lenovo 大神的帖子:https://testerhome.com/topics/9524,此处不再赘述! 14 | b.相关扫盲贴: 15 | https://testerhome.com/topics/9810 16 | http://cdn2.jianshu.io/p/2cbdb50411ae 17 | c.ios-deploy,用于命令安装iOS app ,https://www.npmjs.com/package/ios-deploy 18 | d.FastMonkey设置为非sevrer模式 19 | 20 | ## 3.简单说明下脚本流程 21 | 自动化打包机打包->定时检测最新安装包->自动安装待测app->执行monkey->解析crashreport->DB存储->Web展示 22 | 23 | ## 4.脚本: 24 | https://github.com/Lemonzhulixin/iOS-monkey.git 25 | 26 | ```javascript 27 | # -*- coding: UTF8 -*- 28 | from iOSCrashAnalysis.CrashExport import CrashExport 29 | from iOSCrashAnalysis.getPakeage import getPakeage 30 | from iOSCrashAnalysis import mysql_monkey 31 | from iOSCrashAnalysis.FileOperate import * 32 | from iOSCrashAnalysis.BaseIosPhone import get_ios_devices,get_ios_PhoneInfo 33 | from iOSCrashAnalysis.FileOperate import FileFilt 34 | 35 | 36 | PATH = lambda p: os.path.abspath( 37 | os.path.join(os.path.dirname(__file__), p) 38 | ) 39 | 40 | def monkey(devicename): 41 | cmd_monkey = "xcodebuild -project /Users/iOS_Team/.jenkins/workspace/iOS_Monkey_VivaVideo/XCTestWD/XCTestWD/XCTestWD.xcodeproj " \ 42 | "-scheme XCTestWDUITests " \ 43 | "-destination 'platform=iOS,name=" + devicename + "' " + \ 44 | "XCTESTWD_PORT=8001 " + \ 45 | "clean test" 46 | 47 | print(cmd_monkey) 48 | try: 49 | os.system(cmd_monkey) 50 | except Exception as msg: 51 | print('error message:', msg) 52 | raise 53 | 54 | if __name__ == '__main__': 55 | print('获取设备信息') 56 | # dev_list = [] 57 | # devices = get_ios_devices() 58 | # for i in range(len(devices)): 59 | # duid = get_ios_devices()[i] 60 | # dev = get_ios_PhoneInfo(duid) 61 | # dev_list.append(dev) 62 | # print(dev_list) 63 | 64 | deviceName = 'iPhone2140' 65 | deviceID = 'e80251f0e66967f51add3ad0cdc389933715c3ed' 66 | release = '9.3.2' 67 | 68 | print('远程复制ipa文件到本地') 69 | start_time = time.strftime('%Y%m%d%H%M%S', time.localtime()) 70 | cmd_copy = 'sshpass -p ios scp -r iOS_Team@10.0.35.xx:/Users/iOS_Team/XiaoYing_AutoBuild/XiaoYing/XiaoYingApp/fastlane/output_ipa/ ~/Desktop' 71 | os.system(cmd_copy) 72 | 73 | print('安装ipa测试包到设备') 74 | path = "/Users/iOS_Team/Desktop/output_ipa/" 75 | file_format = ['.ipa'] 76 | ipa_path = getPakeage().get_ipa(path, file_format) 77 | getPakeage().install(path, file_format, deviceID) 78 | 79 | print("启动monkey") 80 | monkey(deviceName) 81 | 82 | print('解析crash report') 83 | find_str = 'XiaoYing-' # 待测app crashreport文件关键字 84 | file_format1 = [".ips"] # 导出的crash文件后缀 85 | file_format2 = [".crash"] # 解析后的crash文件后缀 86 | CrashExport(deviceID, find_str, file_format1, file_format2) 87 | end_time = time.strftime('%Y%m%d%H%M%S', time.localtime()) 88 | 89 | print('测试结果数据解析并DB存储') 90 | loacl_time = time.strftime('%Y%m%d%H%M%S', time.localtime()) 91 | iOS_tag = 'iOS_' + loacl_time 92 | 93 | print('插入数据到device表') 94 | deviceData = { 95 | 'name': deviceName, 96 | 'serial_number': deviceID, 97 | 'version': release, 98 | 'status': 1, 99 | 'tag': 'iOS' 100 | } 101 | 102 | print('插入数据到apk信息表') 103 | # ipa_path = '/Users/zhulixin/Desktop/output_ipa/day_inke_release_xiaoying.ipa' 104 | ipainfo = getPakeage().getIpaInfo(ipa_path) 105 | apkData = { 106 | 'app_name': ipainfo[0], 107 | 'ver_name': ipainfo[2], 108 | 'ver_code': ipainfo[3], 109 | 'file_name': 'day_inke_release_xiaoying.ipa', 110 | 'file_path': ipa_path, 111 | 'build_time': start_time, 112 | 'tag': iOS_tag 113 | } 114 | 115 | print('插入数据到task表') 116 | taskData = { 117 | 'start_time': start_time, 118 | 'end_time': end_time, 119 | 'app_name': ipainfo[0], 120 | 'devices': 1, 121 | 'test_count': None, 122 | 'pass_count': None, 123 | 'fail_count': None, 124 | 'passing_rate': None, 125 | 'tag': iOS_tag, 126 | 'info': None 127 | } 128 | 129 | print('插入数据到results表') 130 | # f = FileFilt() 131 | # f.FindFile(find_str, file_format1, './CrashInfo/') 132 | # crash_count = len(f.fileList) 133 | # result = 1 134 | # if crash_count: 135 | # result = 0 136 | 137 | resultData = { 138 | 'result_id': start_time + '-monkey-' + ipainfo[0], 139 | 'start_time': start_time, 140 | 'end_time': end_time, 141 | 'device_name': deviceName, 142 | 'apk_id': None, 143 | 'result': None, 144 | 'status': None, 145 | 'CRASHs': None, 146 | 'ANRs': None, 147 | 'tag': iOS_tag, 148 | 'device_log':None, 149 | 'monkey_log': None, 150 | 'monkey_loop': None, 151 | 'cmd':None, 152 | 'seed': None 153 | } 154 | 155 | # print('deviceData:', deviceData) 156 | # mysql_monkey.insert_record_to_phones(deviceData) 157 | 158 | print('apkData:', apkData) 159 | mysql_monkey.insert_record_to_apks(apkData) 160 | 161 | print('taskData:', taskData) 162 | mysql_monkey.insert_record_to_tasks(taskData) 163 | 164 | print('resultData:', resultData) 165 | mysql_monkey.insert_record_to_results(resultData) 166 | 167 | print("压缩测试结果并传") 168 | f = FileFilt() 169 | results_file = f.zip_report(loacl_time, './CrashInfo/', './Results_ZIP/') 170 | url = 'http://10.0.32.xx:5100/api/v1/iOS-monkey' 171 | files = {'file': open(results_file, 'rb')} 172 | response = requests.post(url, files=files) 173 | json = response.json() 174 | 175 | print("删除本次的测试结果") 176 | f.DelFolder('./CrashInfo/') 177 | print("xxxxxxxxxxxxxxxxxxxxxxxxx Finish Test xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") 178 | ``` 179 | 180 | ## 5.Jenkins 部署定时任务 181 | 182 | ## 6.待优化 183 | a.多设备执行 184 | b.设备系统日志获取及web展示 185 | c.操作日志获取及web展示 186 | 187 | 最后感谢@zhangzhao_lenovo 开源的FastMonkey工具,赞! 188 | -------------------------------------------------------------------------------- /Results_ZIP/iOS_20181226152906.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemonzhulixin/iOS-monkey/f0a25e7cf10c60a8d53cbd8dc0818226cd17fcdc/Results_ZIP/iOS_20181226152906.zip -------------------------------------------------------------------------------- /iOSCrashAnalysis/BaseIosCrash.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: UTF-8 -*- 3 | import re 4 | import sys, getopt 5 | import subprocess 6 | import os 7 | 8 | PATH = lambda p: os.path.abspath( 9 | os.path.join(os.path.dirname(__file__), p) 10 | ) 11 | 12 | def getUUID(text): 13 | uuid = re.findall('<(.*)>', text)[0].upper() 14 | UUID = uuid[0:8] + '-' + uuid[8:12] + '-' + uuid[12:16] + '-' + uuid[16:20] + "-" + uuid[20:32] 15 | return UUID 16 | 17 | 18 | def analyzeCrashLog(inputfile, outputfile): 19 | crashlog = subprocess.getoutput('grep --after-context=1000 "Binary Images:" ' + inputfile + ' | grep "XiaoYing arm"') 20 | print(crashlog) 21 | uuid = getUUID(crashlog) 22 | print(uuid) 23 | # ttt = commands.getstatus('sshpass -p ios ssh iOS_Team@10.0.35.21') 24 | 25 | path = subprocess.getoutput('mdfind "com_apple_xcode_dsym_uuids == " ' + uuid) 26 | # path = path.replace(' ','\\ ') 27 | path = path + '/dSYMs/XiaoYing.app.dSYM' 28 | 29 | path = "'" + path + "'" 30 | print(path) 31 | analysisPath = '/Users/iOS_Team/.jenkins/workspace/iOS_Monkey_VivaVideo/iOSCrashAnalysis/' 32 | outname = os.path.splitext(inputfile)[0] 33 | 34 | ttt = subprocess.getoutput( 35 | analysisPath + 'symbolicatecrash ' + inputfile + ' -d ' + path + ' -o ' + outname + '.crash') 36 | print(ttt) 37 | 38 | 39 | def main(argv): 40 | inputfile = '' 41 | outputfile = '' 42 | try: 43 | opts, args = getopt.getopt(argv, "hi:o:", ["ifile=", "ofile="]) 44 | except getopt.GetoptError: 45 | sys.exit(2) 46 | 47 | for opt, arg in opts: 48 | if opt == '-h': 49 | sys.exit() 50 | elif opt in ("-i", "--ifile"): 51 | inputfile = arg 52 | elif opt in ("-o", "--ofile"): 53 | outputfile = arg 54 | 55 | print('输入的文件为:', inputfile) 56 | print('输出的文件为:', outputfile) 57 | analyzeCrashLog(inputfile, outputfile) 58 | 59 | 60 | if __name__ == "__main__": 61 | main(sys.argv[1:]) -------------------------------------------------------------------------------- /iOSCrashAnalysis/BaseIosPhone.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | import os 4 | 5 | ''' 6 | 获取ios下的硬件信息 7 | ''' 8 | 9 | 10 | def get_ios_devices(): 11 | devices = [] 12 | result = subprocess.Popen("ideviceinfo -k UniqueDeviceID", shell=True, stdout=subprocess.PIPE, 13 | stderr=subprocess.PIPE).stdout.readlines() 14 | 15 | for item in result: 16 | t = item.decode().split("\n") 17 | if len(t) >= 2: 18 | devices.append(t[0]) 19 | return devices 20 | 21 | 22 | def get_ios_version(duid): 23 | command = "ideviceinfo -u %s -k ProductVersion" % duid 24 | result = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, 25 | stderr=subprocess.PIPE).stdout.readlines() 26 | for item in result: 27 | t = item.decode().split("\n") 28 | if len(t) >= 2: 29 | return t[0] 30 | 31 | 32 | def get_ios_product_name(duid): 33 | command = "ideviceinfo -u %s -k DeviceName" % duid 34 | result = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, 35 | stderr=subprocess.PIPE).stdout.readlines() 36 | for item in result: 37 | t = item.decode().split("\n") 38 | if len(t) >= 2: 39 | return t[0] 40 | 41 | def get_ios_product_type(duid): 42 | command = "ideviceinfo -u %s -k ProductType" % duid 43 | result = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, 44 | stderr=subprocess.PIPE).stdout.readlines() 45 | for item in result: 46 | t = item.decode().split("\n") 47 | if len(t) >= 2: 48 | return t[0] 49 | 50 | def get_ios_product_os(duid): 51 | command = "ideviceinfo -u %s -k ProductName" % duid 52 | result = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, 53 | stderr=subprocess.PIPE).stdout.readlines() 54 | for item in result: 55 | t = item.decode().split("\n") 56 | if len(t) >= 2: 57 | return t[0] 58 | 59 | def get_ios_PhoneInfo(duid): 60 | name = get_ios_product_name(duid) 61 | release = get_ios_version(duid) 62 | type = get_ios_product_type(duid) 63 | result = {"release": release, "device": name, "duid": duid, "type": type} 64 | return result 65 | 66 | #编译facebook的wda到真机 67 | def build_wda_ios(duid): 68 | os.popen( 69 | "xcodebuild -project WebDriverAgent.xcodeproj -scheme WebDriverAgentRunner -destination id=" + duid + " test") 70 | 71 | 72 | if __name__ == '__main__': 73 | dev_list = [] 74 | devices = get_ios_devices() 75 | for i in range(len(devices)): 76 | duid = get_ios_devices()[i] 77 | dev = get_ios_PhoneInfo(duid) 78 | dev_list.append(dev) 79 | print(dev_list) 80 | 81 | 82 | -------------------------------------------------------------------------------- /iOSCrashAnalysis/CrashExport.py: -------------------------------------------------------------------------------- 1 | #coding=utf-8 2 | import os 3 | from iOSCrashAnalysis import FileOperate 4 | 5 | PATH = lambda p: os.path.abspath( 6 | os.path.join(os.path.dirname(__file__), p) 7 | ) 8 | 9 | 10 | def CrashExport(deviceID, find_str, format1, format2): 11 | print("============开始导出crashreport==========") 12 | resultPath = ''.join(['./CrashInfo/']) 13 | beforePath = os.path.join(resultPath + 'temp') 14 | if not os.path.exists(beforePath): 15 | os.makedirs(beforePath) 16 | 17 | afterPath = os.path.join(resultPath) 18 | if not os.path.exists(afterPath): 19 | os.makedirs(afterPath) 20 | print("导出设备中的所有crash文件") 21 | exportReport = 'idevicecrashreport -u ' + deviceID + ' ' + beforePath + '/' 22 | print(exportReport) 23 | os.system(exportReport) # 导出设备中的crash 24 | 25 | print("============开始过滤并解析待测app相关crashreport==========") 26 | f = FileOperate.FileFilt() 27 | f.FindFile(find_str, format1, beforePath) 28 | 29 | # if len(f.fileList) > 0: 30 | # mailpath = '/Users/zhulixin/Desktop/iOS-monkey/iOSCrashAnalysis/crash_mail.py' 31 | # cmd_mail = 'python ' + mailpath + ' "fail" "VivaVideo iOS UI autotest failed" "出现了新的crash,查看地址: http://10.0.32.6:8082/UIAuto/ios"' 32 | # print('发送邮件') 33 | # os.system(cmd_mail) 34 | 35 | for file in f.fileList: 36 | inputFile = os.path.abspath(file) 37 | analysisPath = ''.join(["./iOSCrashAnalysis/"]) 38 | cmd_analysis = 'python3 ' + analysisPath + 'BaseIosCrash.py' + ' -i ' + inputFile 39 | os.system(cmd_analysis) 40 | 41 | # 移动解析完成的crashreport和原始ips文件到新的文件夹 42 | f.MoveFile(find_str, format1, beforePath, afterPath) 43 | f.MoveFile(find_str, format2, beforePath, afterPath) 44 | print("============crashreport解析完成==========") 45 | 46 | # 删除所有解析之前的crash文件,若不想删除,注掉即可 47 | print("============删除所有解析之前的crash文件==========") 48 | f.DelFolder(beforePath) 49 | os.rmdir(beforePath) 50 | 51 | 52 | if __name__ == '__main__': 53 | find_str = 'XiaoYing-' # 待测app crashreport文件关键字 54 | file_format1 = [".ips"] # 导出的crash文件后缀 55 | file_format2 = [".crash"] # 解析后的crash文件后缀 56 | deviceID = 'e80251f0e66967f51add3ad0cdc389933715c3ed' 57 | deviceName = 'iPhone2140' 58 | 59 | CrashExport(deviceID,find_str,file_format1,file_format2) -------------------------------------------------------------------------------- /iOSCrashAnalysis/FileOperate.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import re 4 | import zipfile 5 | import time 6 | import requests 7 | 8 | class FileFilt: 9 | fileList = [] 10 | counter = 0 11 | def __init__(self): 12 | pass 13 | 14 | def FindFile(self, find_str, file_format, path, filtrate=1): 15 | for s in os.listdir(path):#返回指定目录下的所有文件和目录名 16 | newDir = os.path.join(path, s) #将多个路径组合后返回,第一个绝对路径之前的参数将被忽略;os.path.join('路径','文件名.txt') 17 | if os.path.isfile(newDir): #如果path是一个存在的文件,返回True。否则返回False。 18 | if filtrate: 19 | if newDir and (os.path.splitext(newDir)[1] in file_format) \ 20 | and (find_str in os.path.splitext(newDir)[0]): #os.path.splitext():分离文件名与扩展名 21 | self.fileList.append(newDir) 22 | self.counter += 1 23 | else: 24 | self.fileList.append(newDir) 25 | self.counter += 1 26 | 27 | def MoveFile(self, find_str, file_format, path, newpath, filtrate=1): 28 | for s in os.listdir(path): 29 | newDir = os.path.join(path, s) 30 | if os.path.isfile(newDir): 31 | if filtrate: 32 | if newDir and (os.path.splitext(newDir)[1] in file_format) \ 33 | and (find_str in os.path.splitext(newDir)[0]): 34 | self.fileList.append(newDir) 35 | self.counter += 1 36 | shutil.move(newDir, newpath) 37 | else: 38 | self.fileList.append(newDir) 39 | self.counter += 1 40 | 41 | def DelFolder(self, delDir): 42 | delList = os.listdir(delDir) 43 | for f in delList: 44 | filePath = os.path.join(delDir, f) 45 | if os.path.isfile(filePath): 46 | os.remove(filePath) 47 | print(filePath + " was removed!") 48 | elif os.path.isdir(filePath): 49 | shutil.rmtree(filePath, True) 50 | print("Directory: " + filePath + " was removed!") 51 | 52 | def FilePath(self, file_path): 53 | for cur_dir, included_file in os.walk(file_path): 54 | if included_file: 55 | for file in included_file: 56 | print(cur_dir + "\\" + file) 57 | 58 | def zip_report(self,loacl_time, path, newpath): 59 | '''压缩TestReport文件夹 60 | path = "./TestReport" # 要压缩的文件夹路径 61 | newpath = './TestReport_ZIP/' # 压缩后输出文件路径 62 | ''' 63 | if not os.path.exists(newpath): 64 | os.mkdir(newpath) 65 | zipName = newpath + 'iOS_' + loacl_time + '.zip' # 压缩后文件夹的名字 66 | z = zipfile.ZipFile(zipName, 'w', zipfile.ZIP_DEFLATED) # 参数一:文件夹名 67 | for dirpath, dirnames, filenames in os.walk(path): 68 | fpath = dirpath.replace(path, '') 69 | fpath = fpath and fpath + os.sep or '' 70 | for filename in filenames: 71 | z.write(os.path.join(dirpath, filename), fpath + filename) 72 | # z.write(os.path.join(dirpath, filename)) 73 | z.close() 74 | print('Generate zip_report file %s completed........ ' % zipName) 75 | return zipName 76 | 77 | 78 | if __name__ == "__main__": 79 | pass 80 | 81 | # afterPath = '/Users/zhulixin/Desktop/UItest/Results/crashInfo/Before' 82 | # # f = FileFilt() 83 | # # f.FilePath(afterPath) 84 | # 85 | # os.rmdir(afterPath) 86 | 87 | # find_str = 'XiaoYing-' 88 | # file_format = '.ips' 89 | # b = FileFilt() 90 | # b.FindFile(find_str,file_format, path="/Users/zhulixin/new") 91 | # for file in b.fileList: 92 | # filepath = os.path.abspath(file) #绝对路径 93 | # print(filepath) 94 | 95 | 96 | url = 'http://10.0.34.xxx:5100/api/v1/report' 97 | files = {'file': open('/Users/iOS_Team/.jenkins/workspace/iOS_UI_VivaVideo/UItest/Results_ZIP/iOS_20181127171700.zip', 'rb')} 98 | response = requests.post(url, files=files) 99 | json = response.json() 100 | 101 | -------------------------------------------------------------------------------- /iOSCrashAnalysis/__pycache__/BaseIosPhone.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemonzhulixin/iOS-monkey/f0a25e7cf10c60a8d53cbd8dc0818226cd17fcdc/iOSCrashAnalysis/__pycache__/BaseIosPhone.cpython-36.pyc -------------------------------------------------------------------------------- /iOSCrashAnalysis/__pycache__/CrashExport.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemonzhulixin/iOS-monkey/f0a25e7cf10c60a8d53cbd8dc0818226cd17fcdc/iOSCrashAnalysis/__pycache__/CrashExport.cpython-36.pyc -------------------------------------------------------------------------------- /iOSCrashAnalysis/__pycache__/FileOperate.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemonzhulixin/iOS-monkey/f0a25e7cf10c60a8d53cbd8dc0818226cd17fcdc/iOSCrashAnalysis/__pycache__/FileOperate.cpython-36.pyc -------------------------------------------------------------------------------- /iOSCrashAnalysis/__pycache__/getPakeage.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemonzhulixin/iOS-monkey/f0a25e7cf10c60a8d53cbd8dc0818226cd17fcdc/iOSCrashAnalysis/__pycache__/getPakeage.cpython-36.pyc -------------------------------------------------------------------------------- /iOSCrashAnalysis/__pycache__/mysql_monkey.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemonzhulixin/iOS-monkey/f0a25e7cf10c60a8d53cbd8dc0818226cd17fcdc/iOSCrashAnalysis/__pycache__/mysql_monkey.cpython-36.pyc -------------------------------------------------------------------------------- /iOSCrashAnalysis/__pycache__/mysql_operation.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemonzhulixin/iOS-monkey/f0a25e7cf10c60a8d53cbd8dc0818226cd17fcdc/iOSCrashAnalysis/__pycache__/mysql_operation.cpython-36.pyc -------------------------------------------------------------------------------- /iOSCrashAnalysis/crash_mail.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | #-*-coding:utf-8-*- 3 | import smtplib 4 | import os 5 | import sys 6 | 7 | from email.mime.multipart import MIMEMultipart 8 | from email.mime.application import MIMEApplication 9 | from email.mime.text import MIMEText 10 | 11 | #邮件配置 12 | MAIL_FROM_ADDRESS = "xxxxx@quvideo.com" 13 | MAIL_PASSWORD = "Xxxxxx" 14 | SMTP_SERVER = "smtp.exmail.qq.com" 15 | 16 | #接收邮件的邮箱 17 | MAIL_TO_ADDRESS_APPSTORE = ['xxxx.xu@quvideo.com'] 18 | 19 | #发送邮件 20 | #mail_info包含:mail_subject, mail_message 21 | def send_Email(mail_info, receiver): 22 | 23 | print ('*******开始发送邮件****') 24 | 25 | #邮件接受者 26 | mail_receiver = receiver 27 | 28 | #根据不同邮箱配置 host,user,和pwd 29 | mail_host = SMTP_SERVER 30 | mail_port = 465 31 | mail_user = MAIL_FROM_ADDRESS 32 | mail_pwd = MAIL_PASSWORD 33 | 34 | mail_to = ','.join(mail_receiver) 35 | 36 | msg = MIMEMultipart() 37 | 38 | #文本内容 39 | message = mail_info['mail_message'] 40 | subject = mail_info['mail_subject'] 41 | body = MIMEText(message, _subtype='html', _charset='utf-8') 42 | msg.attach(body) 43 | 44 | # 文件类型的附件 45 | appendix = mail_info['mail_file'] 46 | if len(appendix) > 0: 47 | 48 | filename = os.path.basename(appendix) 49 | filepart = MIMEApplication(open(appendix, 'rb').read()) 50 | filepart.add_header('Content-Disposition', 'attachment', filename=filename) 51 | msg.attach(filepart) 52 | 53 | 54 | msg['To'] = mail_to 55 | msg['from'] = mail_user 56 | msg['subject'] = subject 57 | 58 | try: 59 | s = smtplib.SMTP() 60 | # 设置为调试模式,就是在会话过程中会有输出信息 61 | s.set_debuglevel(1) 62 | s.connect(mail_host) 63 | s.login(mail_user, mail_pwd) 64 | s.sendmail(mail_user, mail_receiver, msg.as_string()) 65 | s.close() 66 | 67 | print ('*******邮件发送成功*******') 68 | except Exception as e: 69 | print(e) 70 | 71 | def send_Email_to_developer(mail_info): 72 | send_Email(mail_info, MAIL_TO_ADDRESS_APPSTORE) 73 | 74 | def main(): 75 | print("auto_email") 76 | 77 | if len(sys.argv) > 2: 78 | 79 | #邮件类型 测试成功 测试失败 80 | fuction_type = sys.argv[1] 81 | #邮寄的title 82 | subject = sys.argv[2] 83 | #邮寄的内容 84 | message = sys.argv[3] 85 | #邮件附件 86 | if len(sys.argv) > 4: 87 | appendix = sys.argv[4] 88 | else: 89 | appendix = '' 90 | 91 | if fuction_type == "fail": 92 | 93 | mail_info = { 94 | 'mail_subject' : subject, 95 | 'mail_message' : message, 96 | 'mail_file' : appendix, 97 | } 98 | 99 | send_Email_to_developer(mail_info) 100 | 101 | print("-------------测试出现Crash-------------") 102 | 103 | elif fuction_type == "success": 104 | 105 | mail_info = { 106 | 'mail_subject' : subject, 107 | 'mail_message' : message 108 | } 109 | send_Email_to_developer(mail_info) 110 | 111 | print("-------------success-------------") 112 | else: 113 | print("Fail operation", fuction_type) 114 | else: 115 | print("Fail operation") 116 | 117 | # 执行 118 | main() 119 | 120 | -------------------------------------------------------------------------------- /iOSCrashAnalysis/getPakeage.py: -------------------------------------------------------------------------------- 1 | import os 2 | import zipfile, plistlib, re 3 | 4 | class getPakeage: 5 | def __init__(self): 6 | pass 7 | 8 | def dirlist(self, path, allfile): 9 | filelist = os.listdir(path) 10 | for filename in filelist: 11 | filepath = os.path.join(path, filename) 12 | if os.path.isdir(filepath): 13 | getPakeage().dirlist(filepath, allfile) 14 | else: 15 | allfile.append(filepath) 16 | return allfile 17 | 18 | def get_ipa(self, path,file_format): 19 | files = getPakeage().dirlist(path, []) 20 | ipa_list = [] 21 | for ipa in files: 22 | if (os.path.splitext(ipa)[1] in file_format): 23 | t = os.path.getctime(ipa) 24 | ipa_list.append([ipa, t]) 25 | order = sorted(ipa_list, key=lambda e: e[1], reverse=True) 26 | ipa_path = order[0][0] 27 | return ipa_path 28 | 29 | def install(self,path,file_format,duid): 30 | ipa_path = getPakeage().get_ipa(path,file_format) 31 | cmd = 'ios-deploy –r -b ' + '"' + ipa_path + '"' + ' -i ' + duid 32 | print('安装待测试的app', cmd) 33 | try: 34 | os.system(cmd) 35 | except Exception as msg: 36 | print('error message:', msg) 37 | raise 38 | 39 | def find_plist_path(self,zip_file): 40 | name_list = zip_file.namelist() 41 | pattern = re.compile(r'Payload/[^/]*.app/Info.plist') 42 | for path in name_list: 43 | m = pattern.match(path) 44 | if m is not None: 45 | return m.group() 46 | 47 | def getIpaInfo(self, ipa_path): 48 | ipa_file = zipfile.ZipFile(ipa_path) 49 | plist_path = getPakeage().find_plist_path(ipa_file) 50 | plist_data = ipa_file.read(plist_path) 51 | plist_root = plistlib.loads(plist_data) 52 | 53 | name = plist_root['CFBundleDisplayName'] 54 | bundleID = plist_root['CFBundleIdentifier'] 55 | version = plist_root['CFBundleShortVersionString'] 56 | appKey = plist_root['XiaoYingAppKey'] 57 | miniOSVersion = plist_root['MinimumOSVersion'] 58 | print("=====getIpaInfo=========") 59 | print('appName: %s' % name) 60 | print('bundleId: %s' % bundleID) 61 | print('appVersion: %s' % version) 62 | print('appKey: %s' % appKey) 63 | print('miniOSVersion: %s' % miniOSVersion) 64 | return name, bundleID, version, appKey, miniOSVersion 65 | 66 | if __name__ == '__main__': 67 | 68 | cmd_copy = 'sshpass -p ios scp -r iOS_Team@10.0.35.xx:/Users/iOS_Team/XiaoYing_AutoBuild/XiaoYing/XiaoYingApp/fastlane/output_ipa/ ~/Desktop' 69 | 70 | print('远程复制ipa文件到本地') 71 | os.system(cmd_copy) 72 | 73 | path = "/Users/zhulixin/Desktop/output_ipa/" 74 | file_format = ['.ipa'] 75 | duid = 'abab40339eaf2274aaf1ef068e11d6f85d84aae1' 76 | devicename = 'iPhone2146' 77 | ipa_path = getPakeage().get_ipa(path, file_format) 78 | print(ipa_path) 79 | 80 | getPakeage().install(path, file_format, duid) 81 | 82 | getPakeage().getIpaInfo(ipa_path) 83 | 84 | -------------------------------------------------------------------------------- /iOSCrashAnalysis/mysql_monkey.py: -------------------------------------------------------------------------------- 1 | import mysql.connector 2 | 3 | def upload_sql(sql, value): 4 | db_host = '10.0.32.xxx' 5 | port = 8060 6 | my_connect = mysql.connector.connect( 7 | host=db_host, 8 | port=port, 9 | db='qxxxx', 10 | user='rxxx', 11 | password='rxxx', 12 | charset='utf8' 13 | ) 14 | my_cursor = my_connect.cursor() 15 | try: 16 | my_cursor.execute(sql, value) 17 | my_connect.commit() 18 | except mysql.connector.Error as err: 19 | print("Failed inserting by errorcode {}".format(err)) 20 | my_cursor.close() 21 | my_connect.close() 22 | 23 | 24 | def insert_record_to_phones(value): 25 | """ 26 | 插入数据到phones表 27 | :param value: 28 | value = { 29 | 'name': 'NCE_TL10', 30 | 'serial_number': 'BCD9XA1732301914', 31 | 'version': '6.0', 32 | 'status': 1, 33 | 'tag': 'Android' 34 | } 35 | :return: 36 | """ 37 | my_sql = "INSERT INTO monkey_phones (phone_id, name, serial_number, version, status, tag) " \ 38 | "VALUES (NULL, %(name)s, %(serial_number)s, %(version)s, %(status)s, %(tag)s)" 39 | upload_sql(my_sql, value) 40 | return 41 | 42 | def insert_record_to_apks(value): 43 | """ 44 | 插入数据到apks表 45 | :param value: 46 | value = { 47 | 'app_name': 'com.quvideo.xiaoying', 48 | 'ver_name': '7.5.5', 49 | 'ver_code': '6705050', 50 | 'file_name': 'XiaoYing_V7.5.5_0-xiaoyingtest-OthersAbroadDebug-2018-11-20_08_37_07.apk', 51 | 'file_path': '/Users/iOS_Team/Desktop/QuTestMonkey/app/static/apks/xiaoying/XiaoYing_V7.5.5_0-xiaoyingtest-OthersAbroadDebug-2018-11-20_08_37_07.apk', 52 | 'build_time': datetime.now().strftime('%Y%m%d%H%M%S'), 53 | 'tag': 'Android' 54 | } 55 | :return: 56 | """ 57 | my_sql = "INSERT INTO monkey_apks (apk_id, app_name, ver_name, ver_code, file_name, file_path, build_time, tag) " \ 58 | "VALUES (NULL, %(app_name)s, %(ver_name)s, %(ver_code)s, %(file_name)s, %(file_path)s," \ 59 | " %(build_time)s, %(tag)s)" 60 | upload_sql(my_sql, value) 61 | return 62 | 63 | def insert_record_to_tasks(value): 64 | """ 65 | 插入数据到tasks表 66 | :param value: 67 | value = { 68 | 'start_time': datetime.now().strftime('%Y%m%d%H%M%S'), 69 | 'end_time': datetime.now().strftime('%Y%m%d%H%M%S'), 70 | 'app_name': '小影', 71 | 'devices': None 72 | 'test_count': 1, 73 | 'pass_count': 1, 74 | 'fail_count': 0, 75 | 'passing_rate': 1, 76 | 'tag': datetime.now().strftime('%Y%m%d%H%M%S') + '-monkey', 77 | 'info': None 78 | } 79 | :return: 80 | """ 81 | my_sql = "INSERT INTO monkey_tasks (task_id, start_time, end_time, app_name, devices, test_count, pass_count," \ 82 | " fail_count, passing_rate, info, tag) " \ 83 | "VALUES (NULL, %(start_time)s, %(end_time)s, %(app_name)s, %(devices)s, %(test_count)s," \ 84 | " %(pass_count)s, %(fail_count)s, %(passing_rate)s, %(info)s, %(tag)s)" 85 | upload_sql(my_sql, value) 86 | return 87 | 88 | def insert_record_to_results(value): 89 | """ 90 | 插入数据到results表 91 | :param value: 92 | result_data = { 93 | 'result_id': '20181208163031-monkey-小影-0', 94 | 'start_time': '20181108163032', 95 | 'end_time': None, 96 | 'device_name': 'BIBEYDSCO7OBSWVW', 97 | #1 PASS 98 | 'result': 1, 99 | 'status': 0, 100 | 'CRASHs': 0, 101 | 'ANRs': 0, 102 | 'tag': '20181108163031-monkey', 103 | 'device_log': 'http://10.0.32.6:5100/static/logs/devicelogs/com.quvideo.xiaoying/device-BIBEYDSCO7OBSWVW-20181108163031-小影-0.log', 104 | 'monkey_log': 'http://10.0.32.6:5100/static/logs/monkeylogs/com.quvideo.xiaoying/monkey-BIBEYDSCO7OBSWVW-20181108163031-小影-0.log', 105 | 'monkey_loop': 10, 106 | 'cmd': '--throttle 1000 --pct-touch 70 --pct-motion 5 --pct-trackball 5 --pct-appswitch 20 --kill-process-after-error --monitor-native-crashes --ignore-crashes --ignore-timeouts', 107 | 'seed': 3905 108 | } 109 | :return: 110 | """ 111 | my_sql = "INSERT INTO monkey_results (result_id, start_time, end_time, device_name, apk_id, result, status, CRASHs," \ 112 | " ANRs, tag, device_log, monkey_log, monkey_loop, cmd, seed) " \ 113 | "VALUES (%(result_id)s, %(start_time)s, %(end_time)s, %(device_name)s, %(apk_id)s, %(result)s," \ 114 | " %(status)s, %(CRASHs)s, %(ANRs)s, %(tag)s, %(device_log)s, %(monkey_log)s, %(monkey_loop)s," \ 115 | " %(cmd)s, %(seed)s)" 116 | upload_sql(my_sql, value) 117 | return -------------------------------------------------------------------------------- /iOSCrashAnalysis/symbolicatecrash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | # 3 | # This script parses a crashdump file and attempts to resolve addresses into function names. 4 | # 5 | # It finds symbol-rich binaries by: 6 | # a) searching in Spotlight to find .dSYM files by UUID, then finding the executable from there. 7 | # That finds the symbols for binaries that a developer has built with "DWARF with dSYM File". 8 | # b) searching in various SDK directories. 9 | # 10 | # Copyright (c) 2008-2015 Apple Inc. All Rights Reserved. 11 | # 12 | # 13 | 14 | use strict; 15 | use warnings; 16 | use Getopt::Long; 17 | use Cwd qw(realpath); 18 | use List::MoreUtils qw(uniq); 19 | use File::Basename qw(basename); 20 | use File::Glob ':glob'; 21 | use Env qw(DEVELOPER_DIR); 22 | use Config; 23 | no warnings "portable"; 24 | 25 | require bigint; 26 | if($Config{ivsize} < 8) { 27 | bigint->import(qw(hex)); 28 | } 29 | 30 | ############################# 31 | 32 | # Forward definitons 33 | sub usage(); 34 | 35 | ############################# 36 | 37 | # read and parse command line 38 | my $opt_help = 0; 39 | my $opt_verbose = 0; 40 | my $opt_output = "-"; 41 | my @opt_dsyms = (); 42 | my $opt_spotlight = 1; 43 | 44 | Getopt::Long::Configure ("bundling"); 45 | GetOptions ("help|h" => \$opt_help, 46 | "verbose|v" => \$opt_verbose, 47 | "output|o=s" => \$opt_output, 48 | "dsym|d=s" => \@opt_dsyms, 49 | "spotlight!" => \$opt_spotlight) 50 | or die("Error in command line arguments\n"); 51 | 52 | usage() if $opt_help; 53 | 54 | ############################# 55 | 56 | # have this thing to de-HTMLize Leopard-era plists 57 | my %entity2char = ( 58 | # Some normal chars that have special meaning in SGML context 59 | amp => '&', # ampersand 60 | 'gt' => '>', # greater than 61 | 'lt' => '<', # less than 62 | quot => '"', # double quote 63 | apos => "'", # single quote 64 | ); 65 | 66 | ############################# 67 | 68 | if(!defined($DEVELOPER_DIR)) { 69 | die "Error: \"DEVELOPER_DIR\" is not defined"; 70 | } 71 | 72 | 73 | # We will find these tools once we can guess the right SDK 74 | my $otool = undef; 75 | my $atos = undef; 76 | my $symbolstool = undef; 77 | my $size = undef; 78 | 79 | 80 | ############################# 81 | # run the script 82 | 83 | symbolicate_log(@ARGV); 84 | 85 | exit 0; 86 | 87 | ############################# 88 | 89 | # begin subroutines 90 | 91 | sub HELP_MESSAGE() { 92 | usage(); 93 | } 94 | 95 | sub usage() { 96 | print STDERR < [SYMBOL_PATH ...] 99 | 100 | The crash log to be symbolicated. If "-", then the log will be read from stdin 101 | Additional search paths in which to search for symbol rich binaries 102 | -o | --output The symbolicated log will be written to OUTPUT_FILE. Defaults to "-" (i.e. stdout) if not specified 103 | -d | --dsym Adds additional dSYM that will be consulted if and when a binary's UUID matches (may be specified more than once) 104 | -h | --help Display this help message 105 | -v | --verbose Enables additional output 106 | EOF 107 | exit 1; 108 | } 109 | 110 | ############## 111 | 112 | sub getToolPath { 113 | my ($toolName, $sdkGuess) = @_; 114 | 115 | if (!defined($sdkGuess)) { 116 | $sdkGuess = "macosx"; 117 | } 118 | 119 | my $toolPath = `'/usr/bin/xcrun' -sdk $sdkGuess -find $toolName`; 120 | if (!defined($toolPath) || $? != 0) { 121 | if ($sdkGuess eq "macosx") { 122 | die "Error: can't find tool named '$toolName' in the $sdkGuess SDK or any fallback SDKs"; 123 | } elsif ($sdkGuess eq "iphoneos") { 124 | print STDERR "## Warning: can't find tool named '$toolName' in iOS SDK, falling back to searching the Mac OS X SDK\n"; 125 | return getToolPath($toolName, "macosx"); 126 | } else { 127 | print STDERR "## Warning: can't find tool named '$toolName' in the $sdkGuess SDK, falling back to searching the iOS SDK\n"; 128 | return getToolPath($toolName, "iphoneos"); 129 | } 130 | } 131 | 132 | chomp $toolPath; 133 | print STDERR "$toolName path is '$toolPath'\n" if $opt_verbose; 134 | 135 | return $toolPath; 136 | } 137 | 138 | ############## 139 | 140 | sub getSymbolDirPaths { 141 | my ($hwModel, $osVersion, $osBuild) = @_; 142 | 143 | print STDERR "(\$hwModel, \$osVersion, \$osBuild) = ($hwModel, $osVersion, $osBuild)\n" if $opt_verbose; 144 | 145 | my $versionPattern = "{$hwModel $osVersion ($osBuild),$osVersion ($osBuild),$osVersion,$osBuild}"; 146 | #my $versionPattern = '*'; 147 | print STDERR "\$versionPattern = $versionPattern\n" if $opt_verbose; 148 | 149 | my @result = grep { -e && -d } bsd_glob('{/System,,~}/Library/Developer/Xcode/*DeviceSupport/'.$versionPattern.'/Symbols*', GLOB_BRACE | GLOB_TILDE); 150 | 151 | foreach my $foundPath (`mdfind "kMDItemCFBundleIdentifier == 'com.apple.dt.Xcode' || kMDItemCFBundleIdentifier == 'com.apple.Xcode'"`) { 152 | chomp $foundPath; 153 | my @pathResults = grep { -e && -d && !/Simulator/ } bsd_glob($foundPath.'/Contents/Developer/Platforms/*.platform/DeviceSupport/'.$versionPattern.'/Symbols*/'); 154 | push(@result, @pathResults); 155 | } 156 | 157 | print STDERR "Symbol directory paths: @result\n" if $opt_verbose; 158 | return @result; 159 | } 160 | 161 | sub getSymbolPathAndArchFor_searchpaths { 162 | my ($bin,$path,$build,$uuid,@extra_search_paths) = @_; 163 | my @results; 164 | 165 | if (! (defined $bin && length($bin)) && !(defined $path && length($path)) ) { 166 | return undef; 167 | } 168 | 169 | for my $item (@extra_search_paths) { 170 | my $glob = "$item" . "{"; 171 | if (defined $bin && length($bin)) { 172 | $glob .= "$bin,*/$bin,"; 173 | } 174 | if (defined $path && length($path)) { 175 | $glob .= "$path,"; 176 | } 177 | $glob .= "}*"; 178 | #print STDERR "\nSearching pattern: [$glob]...\n" if $opt_verbose; 179 | push(@results, grep { -e && (! -d) } bsd_glob ($glob, GLOB_BRACE)); 180 | } 181 | 182 | for my $out_path (@results) { 183 | my $arch = archForUUID($out_path, $uuid); 184 | if (defined($arch) && length($arch)) { 185 | return ($out_path, $arch); 186 | } 187 | } 188 | 189 | return undef; 190 | } 191 | 192 | sub getSymbolPathFor_uuid{ 193 | my ($uuid, $uuidsPath) = @_; 194 | $uuid or return undef; 195 | $uuid =~ /(.{4})(.{4})(.{4})(.{4})(.{4})(.{4})(.{8})/; 196 | return Cwd::realpath("$uuidsPath/$1/$2/$3/$4/$5/$6/$7"); 197 | } 198 | 199 | # Convert a uuid from the canonical format, like "C42A118D-722D-2625-F235-7463535854FD", 200 | # to crash log format like "c42a118d722d2625f2357463535854fd". 201 | sub getCrashLogUUIDForCanonicalUUID{ 202 | my ($uuid) = @_; 203 | 204 | $uuid = lc($uuid); 205 | $uuid =~ s/\-//g; 206 | 207 | return $uuid; 208 | } 209 | 210 | # Convert a uuid from the crash log, like "c42a118d722d2625f2357463535854fd", 211 | # to canonical format like "C42A118D-722D-2625-F235-7463535854FD". 212 | sub getCanonicalUUIDForCrashLogUUID{ 213 | my ($uuid) = @_; 214 | 215 | my $cononical_uuid = uc($uuid); # uuid's in Spotlight database and from other tools are all uppercase 216 | $cononical_uuid =~ /(.{8})(.{4})(.{4})(.{4})(.{12})/; 217 | $cononical_uuid = "$1-$2-$3-$4-$5"; 218 | 219 | return $cononical_uuid; 220 | } 221 | 222 | 223 | # Look up a dsym file by UUID in Spotlight, then find the executable from the dsym. 224 | sub getSymbolPathAndArchFor_dsymUuid{ 225 | my ($uuid) = @_; 226 | $uuid or return undef; 227 | 228 | # Convert a uuid from the crash log, like "c42a118d722d2625f2357463535854fd", 229 | # to canonical format like "C42A118D-722D-2625-F235-7463535854FD". 230 | my $canonical_uuid = getCanonicalUUIDForCrashLogUUID($uuid); 231 | 232 | # Do the search in Spotlight. 233 | my $cmd = "mdfind \"com_apple_xcode_dsym_uuids == $canonical_uuid\""; 234 | print STDERR "Running $cmd\n" if $opt_verbose; 235 | 236 | my @dsym_paths = (); 237 | my @archive_paths = (); 238 | 239 | foreach my $dsymdir (split(/\n/, `$cmd`)) { 240 | $cmd = "mdls -name com_apple_xcode_dsym_paths ".quotemeta($dsymdir); 241 | print STDERR "Running $cmd\n" if $opt_verbose; 242 | 243 | my $com_apple_xcode_dsym_paths = `$cmd`; 244 | $com_apple_xcode_dsym_paths =~ s/^com_apple_xcode_dsym_paths\ \= \(\n//; 245 | $com_apple_xcode_dsym_paths =~ s/\n\)//; 246 | 247 | my @subpaths = split(/,\n/, $com_apple_xcode_dsym_paths); 248 | map(s/^[[:space:]]*\"//, @subpaths); 249 | map(s/\"[[:space:]]*$//, @subpaths); 250 | 251 | push(@dsym_paths, map($dsymdir."/".$_, @subpaths)); 252 | 253 | if($dsymdir =~ m/\.xcarchive$/) { 254 | push(@archive_paths, $dsymdir); 255 | } 256 | } 257 | 258 | @dsym_paths = uniq(@dsym_paths); 259 | 260 | if ( @dsym_paths >= 1 ) { 261 | foreach my $dsym_path (@dsym_paths) { 262 | my $arch = archForUUID($dsym_path, $uuid); 263 | if (defined($arch) && length($arch)) { 264 | print STDERR "Found dSYM $dsym_path ($arch)\n" if $opt_verbose; 265 | return ($dsym_path, $arch); 266 | } 267 | } 268 | } 269 | 270 | print STDERR "Did not find dsym for $uuid\n" if $opt_verbose; 271 | return undef; 272 | } 273 | 274 | ######### 275 | 276 | sub archForUUID { 277 | my ($path, $uuid) = @_; 278 | 279 | if ( ! -f $path ) { 280 | print STDERR "## $path doesn't exist \n" if $opt_verbose; 281 | return undef; 282 | } 283 | 284 | my $cmd; 285 | 286 | 287 | $cmd = "/usr/bin/file '$path'"; 288 | print STDERR "Running $cmd\n" if $opt_verbose; 289 | my $file_result = `$cmd`; 290 | my $is_dsym = index($file_result, "dSYM companion file") >= 0; 291 | 292 | my $canonical_uuid = getCanonicalUUIDForCrashLogUUID($uuid); 293 | my $architectures = "armv[4-8][tfsk]?|arm64|i386|x86_64\\S?"; 294 | my $arch; 295 | 296 | $cmd = "'$symbolstool' -uuid '$path'"; 297 | print STDERR "Running $cmd\n" if $opt_verbose; 298 | 299 | my $symbols_result = `$cmd`; 300 | if($symbols_result =~ /$canonical_uuid\s+($architectures)/) { 301 | $arch = $1; 302 | print STDERR "## $path contains $uuid ($arch)\n" if $opt_verbose; 303 | } else { 304 | print STDERR "## $path doesn't contain $uuid\n" if $opt_verbose; 305 | return undef; 306 | } 307 | 308 | $cmd = "'$otool' -arch $arch -l '$path'"; 309 | 310 | print STDERR "Running $cmd\n" if $opt_verbose; 311 | 312 | my $TEST_uuid = `$cmd`; 313 | if ( $TEST_uuid =~ /uuid ((0x[0-9A-Fa-f]{2}\s+?){16})/ || $TEST_uuid =~ /uuid ([^\s]+)\s/ ) { 314 | my $test = $1; 315 | 316 | if ( $test =~ /^0x/ ) { 317 | # old style 0xnn 0xnn 0xnn ... on two lines 318 | $test = join("", split /\s*0x/, $test); 319 | 320 | $test =~ s/0x//g; ## remove 0x 321 | $test =~ s/\s//g; ## remove spaces 322 | } else { 323 | # new style XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX 324 | $test =~ s/-//g; ## remove - 325 | $test = lc($test); 326 | } 327 | 328 | if ( $test eq $uuid ) { 329 | 330 | if ( $is_dsym ) { 331 | return $arch; 332 | } else { 333 | ## See that it isn't stripped. Even fully stripped apps have one symbol, so ensure that there is more than one. 334 | my ($nlocalsym) = $TEST_uuid =~ /nlocalsym\s+([0-9A-Fa-f]+)/; 335 | my ($nextdefsym) = $TEST_uuid =~ /nextdefsym\s+([0-9A-Fa-f]+)/; 336 | my $totalsym = $nextdefsym + $nlocalsym; 337 | print STDERR "\nNumber of symbols in $path: $nextdefsym + $nlocalsym = $totalsym\n" if $opt_verbose; 338 | return $arch if ( $totalsym > 1 ); 339 | 340 | print STDERR "## $path appears to be stripped, skipping.\n" if $opt_verbose; 341 | } 342 | } else { 343 | print STDERR "Given UUID $uuid for '$path' is really UUID $test\n" if $opt_verbose; 344 | } 345 | } else { 346 | print STDERR "Can't understand the output from otool ($TEST_uuid -> $cmd)\n"; 347 | return undef; 348 | } 349 | 350 | return undef; 351 | } 352 | 353 | sub getSymbolPathAndArchFor_manualDSYM { 354 | my ($uuid) = @_; 355 | my @dsym_machos = (); 356 | 357 | for my $dsym_path (@opt_dsyms) { 358 | if( -d $dsym_path ) { 359 | #test_path is a directory, assume it's a dSYM bundle and find the mach-o file(s) within 360 | push @dsym_machos, bsd_glob("$dsym_path/Contents/Resources/DWARF/*"); 361 | next; 362 | } 363 | 364 | if ( -f $dsym_path ) { 365 | #test_path is a file, assume it's a dSYM macho file 366 | push @dsym_machos, $dsym_path; 367 | next; 368 | } 369 | } 370 | 371 | #Check the uuid's of each of the found files 372 | for my $macho_path (@dsym_machos) { 373 | 374 | print STDERR "Checking “$macho_path”\n"; 375 | 376 | my $arch = archForUUID($macho_path, $uuid); 377 | if (defined($arch) && length($arch)) { 378 | print STDERR "$macho_path matches $uuid ($arch)\n"; 379 | return ($macho_path, $arch); 380 | } else { 381 | print STDERR "$macho_path does not match $uuid\n"; 382 | } 383 | } 384 | 385 | return undef; 386 | } 387 | 388 | sub getSymbolPathAndArchFor { 389 | my ($path,$build,$uuid,@extra_search_paths) = @_; 390 | 391 | # derive a few more parameters... 392 | my $bin = ($path =~ /^.*?([^\/]+)$/)[0]; # basename 393 | 394 | # Look in any of the manually-passed dSYMs 395 | if( @opt_dsyms ) { 396 | print STDERR "-- [$uuid] CHECK (manual)\n" if $opt_verbose; 397 | my ($out_path, $arch) = getSymbolPathAndArchFor_manualDSYM($uuid); 398 | if(defined($out_path) && length($out_path) && defined($arch) && length($arch)) { 399 | print STDERR "-- [$uuid] MATCH (manual): $out_path ($arch)\n" if $opt_verbose; 400 | return ($out_path, $arch); 401 | } 402 | print STDERR "-- [$uuid] NO MATCH (manual)\n\n" if $opt_verbose; 403 | } 404 | 405 | # Look for a UUID match in the cache directory 406 | my $uuidsPath = "/Volumes/Build/UUIDToSymbolMap"; 407 | if ( -d $uuidsPath ) { 408 | print STDERR "-- [$uuid] CHECK (uuid cache)\n" if $opt_verbose; 409 | my $out_path = getSymbolPathFor_uuid($uuid, $uuidsPath); 410 | if(defined($out_path) && length($out_path)) { 411 | my $arch = archForUUID($out_path, $uuid); 412 | if (defined($arch) && length($arch)) { 413 | print STDERR "-- [$uuid] MATCH (uuid cache): $out_path ($arch)\n" if $opt_verbose; 414 | return ($out_path, $arch); 415 | } 416 | } 417 | print STDERR "-- [$uuid] NO MATCH (uuid cache)\n\n" if $opt_verbose; 418 | } 419 | 420 | # Look in the search paths (e.g. the device support directories) 421 | print STDERR "-- [$uuid] CHECK (device support)\n" if $opt_verbose; 422 | for my $func ( \&getSymbolPathAndArchFor_searchpaths, ) { 423 | my ($out_path, $arch) = &$func($bin,$path,$build,$uuid,@extra_search_paths); 424 | if ( defined($out_path) && length($out_path) && defined($arch) && length($arch) ) { 425 | print STDERR "-- [$uuid] MATCH (device support): $out_path ($arch)\n" if $opt_verbose; 426 | return ($out_path, $arch); 427 | } 428 | } 429 | print STDERR "-- [$uuid] NO MATCH (device support)\n\n" if $opt_verbose; 430 | 431 | # Ask spotlight 432 | if( $opt_spotlight ) { 433 | print STDERR "-- [$uuid] CHECK (spotlight)\n" if $opt_verbose; 434 | my ($out_path, $arch) = getSymbolPathAndArchFor_dsymUuid($uuid); 435 | 436 | if(defined($out_path) && length($out_path) && defined($arch) && length($arch)) { 437 | print STDERR "-- [$uuid] MATCH (spotlight): $out_path ($arch)\n" if $opt_verbose; 438 | return ($out_path, $arch); 439 | } 440 | print STDERR "-- [$uuid] NO MATCH (spotlight)\n\n" if $opt_verbose; 441 | } 442 | 443 | print STDERR "-- [$uuid] NO MATCH\n\n" if $opt_verbose; 444 | 445 | print STDERR "## Warning: Can't find any unstripped binary that matches version of $path\n" if $opt_verbose; 446 | print STDERR "\n" if $opt_verbose; 447 | 448 | return undef; 449 | } 450 | 451 | ########################### 452 | # crashlog parsing 453 | ########################### 454 | 455 | # options: 456 | # - regex: don't escape regex metas in name 457 | # - continuous: don't reset pos when done. 458 | # - multiline: expect content to be on many lines following name 459 | # - nocolon: when multiline, the header line does not contain a colon 460 | sub parse_section { 461 | my ($log_ref, $name, %arg ) = @_; 462 | my $content; 463 | 464 | $name = quotemeta($name) 465 | unless $arg{regex}; 466 | 467 | my $colon = ':'; 468 | if ($arg{nocolon}) { 469 | $colon = '' 470 | } 471 | 472 | # content is thing from name to end of line... 473 | if( $$log_ref =~ m{ ^($name)$colon [[:blank:]]* (.*?) $ }mgx ) { 474 | $content = $2; 475 | $name = $1; 476 | $name =~ s/^\s+//; 477 | 478 | # or thing after that line. 479 | if($arg{multiline}) { 480 | $content = $1 if( $$log_ref =~ m{ 481 | \G\n # from end of last thing... 482 | (.*?) 483 | (?:\n\s*\n|$) # until next blank line or the end 484 | }sgx ); 485 | } 486 | } 487 | 488 | pos($$log_ref) = 0 489 | unless $arg{continuous}; 490 | 491 | return ($name,$content) if wantarray; 492 | return $content; 493 | } 494 | 495 | # convenience method over above 496 | sub parse_sections { 497 | my ($log_ref,$re,%arg) = @_; 498 | 499 | my ($name,$content); 500 | my %sections = (); 501 | 502 | while(1) { 503 | ($name,$content) = parse_section($log_ref,$re, regex=>1,continuous=>1,%arg); 504 | last unless defined $content; 505 | $sections{$name} = $content; 506 | } 507 | 508 | pos($$log_ref) = 0; 509 | return \%sections; 510 | } 511 | 512 | sub parse_threads { 513 | my ($log_ref,%arg) = @_; 514 | 515 | my $nocolon = 0; 516 | my $stack_delimeter = 'Thread\s+\d+\s?(Highlighted|Crashed)?'; # Crash reports 517 | 518 | if ($arg{event_type}) { 519 | # Spindump reports 520 | if ($arg{event_type} eq "cpu usage" || 521 | $arg{event_type} eq "wakeups" || 522 | $arg{event_type} eq "disk writes" || 523 | $arg{event_type} eq "powerstats") { 524 | 525 | # Microstackshots report 526 | $stack_delimeter = 'Powerstats\sfor:.*'; 527 | $nocolon = 1; 528 | } else { 529 | # Regular spindump 530 | $stack_delimeter = '\s+Thread\s+\S+(\s+DispatchQueue\s+\S+)?'; 531 | $nocolon = 1; 532 | } 533 | } 534 | 535 | return parse_sections($log_ref,$stack_delimeter,multiline=>1,nocolon=>$nocolon) 536 | } 537 | 538 | sub parse_processes { 539 | my ($log_ref, $is_spindump_report, $event_type) = @_; 540 | 541 | if (! $is_spindump_report) { 542 | # Crash Reports only have one process 543 | return ($log_ref); 544 | } 545 | 546 | my $process_delimeter; 547 | 548 | if ($event_type eq "cpu usage" || 549 | $event_type eq "wakeups" || 550 | $event_type eq "disk writes" || 551 | $event_type eq "powerstats") { 552 | 553 | # Microstackshots report 554 | $process_delimeter = '^Powerstats\s+for'; 555 | } else { 556 | # Regular spindump 557 | $process_delimeter = '^Process'; 558 | } 559 | 560 | return \split(/(?=$process_delimeter)/m, $$log_ref); 561 | } 562 | 563 | sub parse_images { 564 | my ($log_ref, $report_version, $is_spindump_report) = @_; 565 | 566 | my $section = parse_section($log_ref,'Binary Images Description',multiline=>1); 567 | if (!defined($section)) { 568 | $section = parse_section($log_ref,'\\s*Binary\\s*Images',multiline=>1,regex=>1); # new format 569 | } 570 | if (!defined($section)) { 571 | die "Error: Can't find \"Binary Images\" section in log file"; 572 | } 573 | 574 | my @lines = split /\n/, $section; 575 | scalar @lines or die "Can't find binary images list: $$log_ref" if !$is_spindump_report; 576 | 577 | my %images = (); 578 | my ($pat, $app, %captures); 579 | 580 | #To get all the architectures for string matching. 581 | my $architectures = "armv[4-8][tfsk]?|arm64|i386|x86_64\\S?"; 582 | 583 | # Once Perl 5.10 becomes the default in Mac OS X, named regexp 584 | # capture buffers of the style (?pattern) would make this 585 | # code much more sane. 586 | if(! $is_spindump_report) { 587 | if($report_version == 102 || $report_version == 103) { # Leopard GM 588 | $pat = ' 589 | ^\s* (\w+) \s* \- \s* (\w+) \s* (?# the range base and extent [1,2] ) 590 | (\+)? (?# the application may have a + in front of the name [3] ) 591 | (.+) (?# bundle name [4] ) 592 | \s+ .+ \(.+\) \s* (?# the versions--generally "??? [???]" ) 593 | \? (?# possible UUID [5] ) 594 | \s* (\/.*)\s*$ (?# first fwdslash to end we hope is path [6] ) 595 | '; 596 | %captures = ( 'base' => \$1, 'extent' => \$2, 'plus' => \$3, 597 | 'bundlename' => \$4, 'uuid' => \$5, 'path' => \$6); 598 | } 599 | elsif($report_version == 104 || $report_version == 105) { # Kirkwood 600 | # 0x182155000 - 0x1824c6fff CoreFoundation arm64 /System/Library/Frameworks/CoreFoundation.framework/CoreFoundation 601 | $pat = ' 602 | ^\s* (\w+) \s* \- \s* (\w+) \s* (?# the range base and extent [1,2] ) 603 | (\+)? (?# the application may have a + in front of the name [3] ) 604 | (.+) (?# bundle name [4] ) 605 | \s+ ('.$architectures.') \s+ (?# the image arch [5] ) 606 | \? (?# possible UUID [6] ) 607 | \s* (\/.*)\s*$ (?# first fwdslash to end we hope is path [7] ) 608 | '; 609 | %captures = ( 'base' => \$1, 'extent' => \$2, 'plus' => \$3, 610 | 'bundlename' => \$4, 'arch' => \$5, 'uuid' => \$6, 611 | 'path' => \$7); 612 | } 613 | else { 614 | die "Unsupported crash log version: $report_version"; 615 | } 616 | } 617 | else { # Spindump reports 618 | # 0x7fffa5f55000 - 0x7fffa63ddff7 com.apple.CoreFoundation 6.9 (1333.19) <08238AC4-4618-39AC-878B-B1562CD6B235> /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation 619 | $pat = ' 620 | ^ (?# Beginning of the line ) 621 | \s* \*? (?# indent and kernel dot) 622 | (\S+) \s* \- \s* (\S+) (?# the range base and extent [1,2] ) 623 | \s+ (.+?) (?# bundle name [3] ) 624 | (?: \s+ (\S+) )? (?# optional short version [4] ) 625 | (?: \s+ \( (\S+) \) )? (?# optional version [5] ) 626 | \s+ \< ( .* ) \> (?# UUID [6] ) 627 | (?: \s+ (\/.*) )? (?# optional path [7] ) 628 | \s*$ (?# End of the line ) 629 | '; 630 | %captures = ( 'base' => \$1, 'extent' => \$2, 'bundleid' => \$3, 631 | 'shortversion' => \$4, 'version' => \$5, 'uuid' => \$6, 632 | 'path' => \$7); 633 | } 634 | 635 | for my $line (@lines) { 636 | next if $line =~ /PEF binary:/; # ignore these 637 | 638 | $line =~ s/(&(\w+);?)/$entity2char{$2} || $1/eg; 639 | 640 | if ($line =~ /$pat/ox) { 641 | 642 | # Dereference references 643 | my %image; 644 | while((my $key, my $val) = each(%captures)) { 645 | $image{$key} = ${$captures{$key}} || ''; 646 | #print STDERR "image{$key} = $image{$key}\n"; 647 | } 648 | 649 | if (defined $image{bundleid} && $image{bundleid} eq "???") { 650 | delete $image{bundleid}; 651 | } 652 | 653 | if (! defined $image{bundlename}) { 654 | # (Only occurs in spindump) 655 | # Match what string frames will use as the binary's identifier 656 | if (defined $image{path} && $image{path} ne '') { 657 | $image{bundlename} = ($image{path} =~ /^.*?([^\/]+)$/)[0]; # basename of path 658 | } elsif (defined $image{bundleid} && $image{bundleid} ne '') { 659 | $image{bundlename} = $image{bundleid}; 660 | } else { 661 | $image{bundlename} = "<$image{uuid}>"; 662 | } 663 | } 664 | 665 | if ($image{extent} eq "???") { 666 | $image{extent} = ''; 667 | } 668 | 669 | # Spindump uses canonical UUID, but the rest of the code here expects CrashLog style UUIDs 670 | $image{uuid} = getCrashLogUUIDForCanonicalUUID($image{uuid}); 671 | 672 | # Just take the first instance. That tends to be the app. 673 | my $bundlename = $image{bundlename}; 674 | $app = $bundlename if (!defined $app && defined $image{plus} && length $image{plus}); 675 | 676 | # frameworks and apps (and whatever) may share the same name, so disambiguate 677 | if ( defined($images{$bundlename}) ) { 678 | # follow the chain of hash items until the end 679 | my $nextIDKey = $bundlename; 680 | while ( length($nextIDKey) ) { 681 | last if ( !length($images{$nextIDKey}{nextID}) ); 682 | $nextIDKey = $images{$nextIDKey}{nextID}; 683 | } 684 | 685 | # add ourselves to that chain 686 | $images{$nextIDKey}{nextID} = $image{base}; 687 | 688 | # and store under the key we just recorded 689 | $bundlename = $bundlename . $image{base}; 690 | } 691 | 692 | # we are the end of the nextID chain 693 | $image{nextID} = ""; 694 | 695 | $images{$bundlename} = \%image; 696 | } 697 | } 698 | 699 | return (\%images, $app); 700 | } 701 | 702 | # if this is actually a partial binary identifier we know about, then 703 | # return the full name. else return undef. 704 | my %_partial_cache = (); 705 | sub resolve_partial_id { 706 | my ($bundle,$images) = @_; 707 | # is this partial? note: also stripping elipsis here 708 | return undef unless $bundle =~ s/^\.\.\.//; 709 | return $_partial_cache{$bundle} if exists $_partial_cache{$bundle}; 710 | 711 | my $re = qr/\Q$bundle\E$/; 712 | for (keys %$images) { 713 | if( /$re/ ) { 714 | $_partial_cache{$bundle} = $_; 715 | return $_; 716 | } 717 | } 718 | return undef; 719 | } 720 | 721 | sub fixup_last_exception_backtrace { 722 | my ($log_ref,$exception,$images) = @_; 723 | my $repl = $exception; 724 | if ($exception =~ m/^.0x/) { 725 | my @lines = split / /, substr($exception, 1, length($exception)-2); 726 | my $counter = 0; 727 | $repl = ""; 728 | for my $line (@lines) { 729 | my ($image,$image_base) = findImageByAddress($images, $line); 730 | my $offset = hex($line) - hex($image_base); 731 | my $formattedTrace = sprintf("%-3d %-30s\t0x%08x %s + %d", $counter, $image, hex($line), $image_base, $offset); 732 | $repl .= $formattedTrace . "\n"; 733 | ++$counter; 734 | } 735 | $log_ref = replace_chunk($log_ref, $exception, $repl); 736 | # may need to do this a second time since there could be First throw call stack too 737 | $log_ref = replace_chunk($log_ref, $exception, $repl); 738 | } 739 | return ($log_ref, $repl); 740 | } 741 | 742 | #sub parse_last_exception_backtrace { 743 | # print STDERR "Parsing last exception backtrace\n" if $opt_verbose; 744 | # my ($backtrace,$images, $inHex) = @_; 745 | # my @lines = split /\n/,$backtrace; 746 | # 747 | # my %frames = (); 748 | # 749 | # # these two have to be parallel; we'll lookup by hex, and replace decimal if needed 750 | # my @hexAddr; 751 | # my @replAddr; 752 | # 753 | # for my $line (@lines) { 754 | # # end once we're done with the frames 755 | # last if $line =~ /\)/; 756 | # last if !length($line); 757 | # 758 | # if ($inHex && $line =~ /0x([[:xdigit:]]+)/) { 759 | # push @hexAddr, sprintf("0x%08s", $1); 760 | # push @replAddr, "0x".$1; 761 | # } 762 | # elsif ($line =~ /(\d+)/) { 763 | # push @hexAddr, sprintf("0x%08x", $1); 764 | # push @replAddr, $1; 765 | # } 766 | # } 767 | # 768 | # # we don't have a hint as to the binary assignment of these frames 769 | # # map_addresses will do it for us 770 | # return map_addresses(\@hexAddr,$images,\@replAddr); 771 | #} 772 | 773 | # returns an oddly-constructed hash: 774 | # 'string-to-replace' => { bundle=>..., address=>... } 775 | sub parse_backtrace { 776 | my ($backtrace,$images,$decrement,$is_spindump_report) = @_; 777 | my @lines = split /\n/,$backtrace; 778 | 779 | my %frames = (); 780 | 781 | if ( ! $is_spindump_report ) { 782 | # Crash report 783 | 784 | my $is_first = 1; 785 | 786 | for my $line (@lines) { 787 | if( $line =~ m{ 788 | ^\d+ \s+ # stack frame number 789 | (\S.*?) \s+ # bundle [1] 790 | ( # description to replace [2] 791 | (0x\w+) \s+ # address [3] 792 | 0x\w+ \s+ # library address 793 | (?: \+ \s+ (\d+))? # offset [4], optional 794 | .* # remainder of description 795 | ) # end of capture 796 | \s* # new line 797 | $ # end of line 798 | }x ) { 799 | my($bundle,$replace,$address,$offset) = ($1,$2,$3,$4); 800 | #print STDERR "Parse_bt: $bundle,$replace,$address\n" if ($opt_verbose); 801 | 802 | # disambiguate within our hash of binaries 803 | $bundle = findImageByNameAndAddress($images, $bundle, $address); 804 | 805 | # skip unless we know about the image of this frame 806 | next unless 807 | $$images{$bundle} or 808 | $bundle = resolve_partial_id($bundle,$images); 809 | 810 | my $raw_address = $address; 811 | if($decrement && !$is_first) { 812 | $address = sprintf("0x%X", (hex($address) & ~1) - 1); 813 | } 814 | 815 | $frames{$replace} = { 816 | 'address' => $address, 817 | 'raw_address' => $raw_address, 818 | 'bundle' => $bundle, 819 | }; 820 | 821 | if (defined $offset) { 822 | $frames{$replace}{offset} = $offset 823 | } 824 | 825 | $is_first = 0; 826 | } 827 | # else { print STDERR "unable to parse backtrace line $line\n" } 828 | } 829 | 830 | } else { 831 | # Spindump report 832 | 833 | my $previousFrame; 834 | my $previousIndentLength; 835 | 836 | for my $line (@lines) { 837 | # *138 unix_syscall64 + 675 (systemcalls.c:376,10 in kernel.development + 6211555) [0xffffff80007ec7e3] 1-138 838 | if( $line =~ m{ 839 | ^ # Start of line 840 | ( \s* \*? ) # indent and kernel dot [1] 841 | ( \d+ ) \s+ # count [2] 842 | ( # Start of string to replace (symbol, binary, address) [3] 843 | ( .+? ) # symbol [4] 844 | (?: \s* \+ \s* (\d+) )? # offset from symbol [5], optional 845 | (?: \s+ \( # Start of binary info, entire section optional 846 | (?: ( .*? ) \s+ in \s+ )? # source info [6], optional 847 | (.+?) # Binary name (or UUID, if no name) [7] 848 | (?: \s* \+ \s* (\d+) )? # Offset in binary [8], optional 849 | \) )? # End of binary info, entire section optional 850 | \s* \[ (.+) \] # address [9] 851 | ) # End of string to replace 852 | (?: \s+ \(.*\) )? # state [10], optional 853 | (?: \s+ # Start of timeline info, entire section optional 854 | (\d+) # Start time index [11] 855 | (?: \s* \- \s* (\d+))? # End time index [12], optional 856 | )? # End of timeline info, entire section optional 857 | $ # End of line 858 | }x ) { 859 | my($indent,$count,$replace,$symbol,$offsetInSymbol,$sourceInfo,$binaryName,$offsetInBinary,$address,$state,$timeIndexStart,$timeIndexEnd) = ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11); 860 | # print STDERR "Parse_bt $line:\n$indent,$count,$symbol,$offsetInSymbol,$sourceInfo,$binaryName,$offsetInBinary,$address,$timeIndexStart,$timeIndexEnd\n" if ($opt_verbose); 861 | 862 | next if defined $sourceInfo; # Don't bother trying to sybolicate frames that already have source info 863 | 864 | next unless defined $binaryName; 865 | 866 | # disambiguate within our hash of binaries 867 | my $binaryKey = findImageByNameAndAddress($images, $binaryName, $address); 868 | 869 | # skip unless we know about the image of this frame 870 | next unless 871 | $$images{$binaryName}; 872 | 873 | $frames{$replace} = { 874 | 'address' => $address, # To be fixed up for non-leaf frames in the next loop 875 | 'raw_address' => $address, 876 | 'bundle' => $binaryKey, 877 | }; 878 | 879 | # Fixed up symbolication address the non-leaf previous frame 880 | if (defined $previousFrame && defined $previousIndentLength && 881 | length $indent > $previousIndentLength) { 882 | 883 | $$previousFrame{'address'} = sprintf("0x%X", (hex($$previousFrame{'address'}) & ~1) - 1); 884 | 885 | # print STDERR "Updated symbolication address: $$previousFrame{'raw_address'} -> $$previousFrame{'address'}\n"; 886 | } 887 | $previousIndentLength = length $indent; 888 | $previousFrame = $frames{$replace}; 889 | } 890 | # else { print STDERR "unable to parse backtrace line $line\n" } 891 | } 892 | 893 | 894 | } 895 | 896 | return \%frames; 897 | } 898 | 899 | sub slurp_file { 900 | my ($file) = @_; 901 | my $data; 902 | my $fh; 903 | my $readingFromStdin = 0; 904 | 905 | local $/ = undef; 906 | 907 | # - or "" mean read from stdin, otherwise use the given filename 908 | if($file && $file ne '-') { 909 | open $fh,"<",$file or die "while reading $file, $! : "; 910 | } else { 911 | open $fh,"<&STDIN" or die "while readin STDIN, $! : "; 912 | $readingFromStdin = 1; 913 | } 914 | 915 | $data = <$fh>; 916 | 917 | 918 | # Replace DOS-style line endings 919 | $data =~ s/\r\n/\n/g; 920 | 921 | # Replace Mac-style line endings 922 | $data =~ s/\r/\n/g; 923 | 924 | # Replace "NO-BREAK SPACE" (these often get inserted when copying from Safari) 925 | # \xC2\xA0 == U+00A0 926 | $data =~ s/\xc2\xa0/ /g; 927 | 928 | close $fh or die $!; 929 | return \$data; 930 | } 931 | 932 | sub parse_OSVersion { 933 | my ($log_ref) = @_; 934 | my $section = parse_section($log_ref,'OS Version'); 935 | if ( $section =~ /\s([0-9\.]+)\s+\(Build (\w+)/ ) { 936 | return ($1, $2) 937 | } 938 | if ( $section =~ /\s([0-9\.]+)\s+\((\w+)/ ) { 939 | return ($1, $2) 940 | } 941 | if ( $section =~ /\s([0-9\.]+)/ ) { 942 | return ($1, "") 943 | } 944 | die "Error: can't parse OS Version string $section"; 945 | } 946 | 947 | sub parse_HardwareModel { 948 | my ($log_ref) = @_; 949 | my $model = parse_section($log_ref, 'Hardware Model'); 950 | if (!defined($model)) { 951 | $model = parse_section($log_ref, 'Hardware model'); # spindump format 952 | } 953 | 954 | $model or return undef; 955 | # HACK: replace the comma in model names because bsd_glob can't handle commas (even escaped ones) in 956 | # the {} groups 957 | $model =~ s/,/\?/g; 958 | $model =~ /(\S+)/; 959 | return $1; 960 | } 961 | 962 | sub parse_SDKGuess { 963 | my ($log_ref) = @_; 964 | 965 | # It turns out that most SDKs are named "lowercased(HardwareModelWithoutNumbers) + os", 966 | # so attempt to form a valid SDK name from that. Any code that uses this must NOT rely 967 | # on this guess being accurate and should fallback to whatever logic makes sense for the situation 968 | my $model = parse_HardwareModel($log_ref); 969 | $model or return undef; 970 | 971 | $model =~ /(\D+)\d/; 972 | $1 or return undef; 973 | 974 | my $sdk = lc($1) . "os"; 975 | if($sdk eq "ipodos" || $sdk eq "ipados") { 976 | $sdk = "iphoneos"; 977 | } 978 | if ( $sdk =~ /mac/) { 979 | $sdk = "macosx"; 980 | } 981 | 982 | return $sdk; 983 | } 984 | 985 | sub parse_event_type { 986 | my ($log_ref) = @_; 987 | my $event = parse_section($log_ref,'Event'); 988 | return $event; 989 | } 990 | 991 | sub parse_steps { 992 | my ($log_ref) = @_; 993 | my $steps = parse_section($log_ref,'Steps'); 994 | $steps or return undef; 995 | $steps =~ /(\d+)/; 996 | return $1; 997 | } 998 | 999 | sub parse_report_version { 1000 | my ($log_ref) = @_; 1001 | my $version = parse_section($log_ref,'Report Version'); 1002 | $version or return undef; 1003 | $version =~ /(\d+)/; 1004 | return $1; 1005 | } 1006 | sub findImageByAddress { 1007 | my ($images,$address) = @_; 1008 | my $image; 1009 | 1010 | for $image (values %$images) { 1011 | if ( hex($address) >= hex($$image{base}) && hex($address) <= hex($$image{extent}) ) 1012 | { 1013 | return ($$image{bundlename},$$image{base}); 1014 | } 1015 | } 1016 | 1017 | print STDERR "Unable to map $address\n" if $opt_verbose; 1018 | 1019 | return undef; 1020 | } 1021 | 1022 | sub findImageByNameAndAddress { 1023 | my ($images,$bundle,$address) = @_; 1024 | my $key = $bundle; 1025 | 1026 | #print STDERR "findImageByNameAndAddress($bundle,$address) ... "; 1027 | 1028 | my $binary = $$images{$bundle}; 1029 | 1030 | while($$binary{nextID} && length($$binary{nextID}) ) { 1031 | last if ( hex($address) >= hex($$binary{base}) && hex($address) <= hex($$binary{extent}) ); 1032 | 1033 | $key = $key . $$binary{nextID}; 1034 | $binary = $$images{$key}; 1035 | } 1036 | 1037 | #print STDERR "$key\n"; 1038 | return $key; 1039 | } 1040 | 1041 | sub prune_used_images { 1042 | my ($images,$bt) = @_; 1043 | 1044 | # make a list of images actually used in backtrace 1045 | my $images_used = {}; 1046 | for(values %$bt) { 1047 | #print STDERR "Pruning: $images, $$_{bundle}, $$_{address}\n" if ($opt_verbose); 1048 | my $imagename = findImageByNameAndAddress($images, $$_{bundle}, $$_{address}); 1049 | $$images_used{$imagename} = $$images{$imagename}; 1050 | } 1051 | 1052 | # overwrite the incoming image list with that; 1053 | %$images = %$images_used; 1054 | } 1055 | 1056 | # fetch symbolled binaries 1057 | # array of binary image ranges and names 1058 | # the OS build 1059 | # the name of the crashed program 1060 | # undef 1061 | # array of possible directories to locate symboled files in 1062 | sub fetch_symbolled_binaries { 1063 | our %uuid_cache; # Global cache of UUIDs we've already searched for 1064 | 1065 | print STDERR "Finding Symbols:\n" if $opt_verbose; 1066 | 1067 | my ($images,$build,$bundle,@extra_search_paths) = @_; 1068 | 1069 | # fetch paths to symbolled binaries. or ignore that lib if we can't 1070 | # find it 1071 | for my $b (keys %$images) { 1072 | my $lib = $$images{$b}; 1073 | my $symbol; 1074 | my $arch; 1075 | 1076 | if (defined $uuid_cache{$$lib{uuid}}) { 1077 | ($symbol, $arch) = @{$uuid_cache{$$lib{uuid}}}; 1078 | if ( $symbol ) { 1079 | $$lib{symbol} = $symbol; 1080 | if ( ! (defined $$lib{arch} && length $$lib{arch}) ) { 1081 | if (defined $arch && length($arch)) { 1082 | print STDERR "Already found $b: @{$uuid_cache{$$lib{uuid}}}\n" if $opt_verbose; 1083 | $$lib{arch} = $arch; 1084 | } else { 1085 | print STDERR "Already checked and failed to find $b (found $symbol, nob can't determine arch)\n" if $opt_verbose; 1086 | delete $$images{$b}; 1087 | next; 1088 | } 1089 | } else { 1090 | print STDERR "Already found $b: @{$uuid_cache{$$lib{uuid}}}\n" if $opt_verbose; 1091 | } 1092 | } else { 1093 | print STDERR "Already checked and failed to find $b\n" if $opt_verbose; 1094 | delete $$images{$b}; 1095 | next; 1096 | } 1097 | } else { 1098 | 1099 | 1100 | print STDERR "-- [$$lib{uuid}] fetching symbol file for $b\n" if $opt_verbose; 1101 | 1102 | $symbol = $$lib{symbol}; 1103 | if ($symbol) { 1104 | print STDERR "-- [$$lib{uuid}] found in cache\n" if $opt_verbose; 1105 | } else { 1106 | ($symbol, $arch) = getSymbolPathAndArchFor($$lib{path},$build,$$lib{uuid},@extra_search_paths); 1107 | @{$uuid_cache{$$lib{uuid}}} = ($symbol, $arch); 1108 | if ( $symbol ) { 1109 | $$lib{symbol} = $symbol; 1110 | if ( ! (defined $$lib{arch} && length $$lib{arch}) ) { 1111 | if (defined $arch && length($arch)) { 1112 | print STDERR "Set $$lib{uuid} to $arch\n" if $opt_verbose; 1113 | $$lib{arch} = $arch; 1114 | } else { 1115 | delete $$images{$b}; 1116 | next; 1117 | } 1118 | } 1119 | } else { 1120 | delete $$images{$b}; 1121 | next; 1122 | } 1123 | } 1124 | } 1125 | 1126 | # check for sliding. set slide offset if so 1127 | open my($ph),"-|", "'$size' -m -l -x '$symbol'" or die $!; 1128 | my $real_base = ( 1129 | grep { $_ } 1130 | map { (/_TEXT.*vmaddr\s+(\w+)/)[0] } <$ph> 1131 | )[0]; 1132 | close $ph; 1133 | if ($?) { 1134 | 1135 | # 13T5280f: My crash logs aren't symbolicating 1136 | # System libraries were not being symbolicated because /usr/bin/size is always failing. 1137 | # That's /usr/bin/size doesn't like LC_SEGMENT_SPLIT_INFO command 12 1138 | # 1139 | # Until that's fixed, just hope for the best and assume no sliding. I've been informed that since 1140 | # this scripts always deals with post-mortem crash files instead of running processes, sliding shouldn't 1141 | # happen in practice. Nevertheless, we should probably add this sanity check back in once we 21604022 1142 | # gets resolved. 1143 | $real_base = $$lib{base} 1144 | 1145 | # call to size failed. Don't use this image in symbolication; don't die 1146 | # delete $$images{$b}; 1147 | #print STDERR "Error in symbol file for $symbol\n"; # and log it 1148 | # next; 1149 | } 1150 | 1151 | if($$lib{base} ne $real_base) { 1152 | $$lib{slide} = hex($real_base) - hex($$lib{base}); 1153 | } 1154 | } 1155 | 1156 | print STDERR keys(%$images) . " binary images were found.\n" if $opt_verbose; 1157 | } 1158 | 1159 | # run atos 1160 | sub symbolize_frames { 1161 | my ($images,$bt,$is_spindump_report) = @_; 1162 | 1163 | # create mapping of framework => address => bt frame (adjust for slid) 1164 | # and for framework => arch 1165 | my %frames_to_lookup = (); 1166 | my %arch_map = (); 1167 | my %base_map = (); 1168 | my %image_map = (); 1169 | 1170 | for my $k (keys %$bt) { 1171 | my $frame = $$bt{$k}; 1172 | my $lib = $$images{$$frame{bundle}}; 1173 | unless($lib) { 1174 | # don't know about it, can't symbol 1175 | # should have already been warned about this! 1176 | # print STDERR "Skipping unknown $$frame{bundle}\n"; 1177 | delete $$bt{$k}; 1178 | next; 1179 | } 1180 | 1181 | # list of address to lookup, mapped to the frame object, for 1182 | # each library 1183 | $frames_to_lookup{$$lib{symbol}}{$$frame{address}} = $frame; 1184 | $arch_map{$$lib{symbol}} = $$lib{arch}; 1185 | $base_map{$$lib{symbol}} = $$lib{base}; 1186 | $image_map{$$lib{symbol}} = $lib; 1187 | } 1188 | 1189 | # run atos for each library 1190 | while(my($symbol,$frames) = each(%frames_to_lookup)) { 1191 | # escape the symbol path if it contains single quotes 1192 | my $escapedSymbol = $symbol; 1193 | $escapedSymbol =~ s/\'/\'\\'\'/g; 1194 | 1195 | # run atos with the addresses and binary files we just gathered 1196 | my $arch = $arch_map{$symbol}; 1197 | my $base = $base_map{$symbol}; 1198 | my $lib = $image_map{$symbol}; 1199 | my $cmd = "'$atos' -arch $arch -l $base -o '$escapedSymbol' @{[ keys %$frames ]} | "; 1200 | 1201 | print STDERR "Running $cmd\n" if $opt_verbose; 1202 | 1203 | open my($ph),$cmd or die $!; 1204 | my @symbolled_frames = map { chomp; $_ } <$ph>; 1205 | close $ph or die $!; 1206 | 1207 | my $references = 0; 1208 | 1209 | foreach my $symbolled_frame (@symbolled_frames) { 1210 | 1211 | my ($library, $source) = ($symbolled_frame =~ /\s*\(in (.*?)\)(?:\s*\((.*?)\))?/); 1212 | $symbolled_frame =~ s/\s*\(in .*?\)//; # clean up -- don't need to repeat the lib here 1213 | 1214 | if ($is_spindump_report) { 1215 | # Source is formatted differently for spindump 1216 | $symbolled_frame =~ s/\s*\(.*?\)//; # remove source info from symbol string 1217 | 1218 | # Spindump may not have had library names, pick them up here 1219 | if (defined $library && !(defined $$lib{path} && length($$lib{path})) && !(defined $$lib{new_path} && length($$lib{new_path})) ) { 1220 | $$lib{new_path} = $library; 1221 | print STDERR "Found new name for $$lib{uuid}: $$lib{new_path}\n" if ( $opt_verbose ); 1222 | } 1223 | } 1224 | 1225 | 1226 | # find the correct frame -- the order should match since we got the address list with keys 1227 | my ($k,$frame) = each(%$frames); 1228 | 1229 | if ( $symbolled_frame !~ /^\d/ ) { 1230 | # only symbolicate if we fetched something other than an address 1231 | 1232 | my $offset = $$frame{offset}; 1233 | if (defined $offset) { 1234 | # add offset from unsymbolicated frame after symbolicated name 1235 | $symbolled_frame =~ s|(.+)\(|$1."+ ".$offset." ("|e; 1236 | } 1237 | 1238 | if ($is_spindump_report) { 1239 | # Spindump formatting 1240 | if (defined $library) { 1241 | $symbolled_frame .= " ("; 1242 | if (defined $source) { 1243 | $symbolled_frame .= "$source in "; 1244 | } 1245 | $symbolled_frame .= "$library + " . (hex($$frame{raw_address}) - hex($base)) . ")"; 1246 | } 1247 | $symbolled_frame .= " [$$frame{raw_address}]"; 1248 | } 1249 | 1250 | $$frame{symbolled} = $symbolled_frame; 1251 | $references++; 1252 | } 1253 | 1254 | } 1255 | 1256 | if ( $references == 0 ) { 1257 | if ( ! $is_spindump_report) { # Bad addresses aren't uncommon in microstackshots and stackshots 1258 | print STDERR "## Warning: Unable to symbolicate from required binary: $symbol\n"; 1259 | } 1260 | } 1261 | } 1262 | 1263 | # just run through and remove elements for which we didn't find a 1264 | # new mapping: 1265 | while(my($k,$v) = each(%$bt)) { 1266 | delete $$bt{$k} unless defined $$v{symbolled}; 1267 | } 1268 | } 1269 | 1270 | # run the final regex to symbolize the log 1271 | sub replace_symbolized_frames { 1272 | my ($log_ref,$bt,$images,$is_spindump_report) = @_; 1273 | my $re = join "|" , map { quotemeta } keys %$bt; 1274 | 1275 | # spindump's symbolled string already includes the raw address 1276 | my $log = $$log_ref; 1277 | $log =~ s#$re# 1278 | my $frame = $$bt{$&}; 1279 | (! $is_spindump_report ? $$frame{raw_address} . " " : "") . $$frame{symbolled}; 1280 | #esg; 1281 | 1282 | $log =~ s/(&(\w+);?)/$entity2char{$2} || $1/eg; 1283 | 1284 | 1285 | if ($is_spindump_report) { 1286 | # Spindump may not have image names, so add any names we found 1287 | 1288 | my @images_to_replace_keys = grep { defined $$images{$_}{new_path} } keys %$images; 1289 | 1290 | if (scalar(@images_to_replace_keys)) { 1291 | 1292 | print STDERR "" . scalar(@images_to_replace_keys) . " images with new names:\n" if ( $opt_verbose ); 1293 | if ( $opt_verbose ) { print STDERR "$_\n" for @images_to_replace_keys; } 1294 | 1295 | # First, replace in frames that we couldn't symbolicate 1296 | # 2 ??? ( + 196600) [0x1051e3ff8] 1297 | # becomes 1298 | # 2 ??? (BackBoard + 196600) [0x1051e3ff8] 1299 | my $image_re = join "|" , map { quotemeta } @images_to_replace_keys; 1300 | $image_re = "\\(($image_re)"; # Open paren precedes UUID in frames 1301 | 1302 | $log =~ s#$image_re# 1303 | "(" . $$images{$1}{new_path} 1304 | #esg; 1305 | 1306 | $log =~ s/(&(\w+);?)/$entity2char{$2} || $1/eg; 1307 | 1308 | # Second, replace in image infos 1309 | # 0x1051b4000 - ??? ??? 1310 | # becomes 1311 | # 0x1051b4000 - ??? ??? BackBoard 1312 | $image_re = join "|" , map { quotemeta } @images_to_replace_keys; 1313 | $image_re = "\\s($image_re)"; # Whitespace precedes image infos 1314 | 1315 | $log =~ s#$image_re# 1316 | "$& " . $$images{$1}{new_path} 1317 | #esg; 1318 | 1319 | $log =~ s/(&(\w+);?)/$entity2char{$2} || $1/eg; 1320 | 1321 | } 1322 | } 1323 | 1324 | 1325 | return \$log; 1326 | } 1327 | 1328 | sub replace_chunk { 1329 | my ($log_ref,$old,$new) = @_; 1330 | my $log = $$log_ref; 1331 | my $re = quotemeta $old; 1332 | $log =~ s/$re/$new/; 1333 | return \$log; 1334 | } 1335 | 1336 | ############# 1337 | 1338 | sub output_log($) { 1339 | my ($log_ref) = @_; 1340 | 1341 | if($opt_output && $opt_output ne "-") { 1342 | close STDOUT; 1343 | open STDOUT, '>', $opt_output; 1344 | } 1345 | 1346 | print $$log_ref; 1347 | } 1348 | 1349 | ############# 1350 | 1351 | sub symbolicate_log { 1352 | my ($file,@extra_search_paths) = @_; 1353 | 1354 | print STDERR "Symbolicating $file ...\n" if ( $opt_verbose && defined $file); 1355 | print STDERR "Symbolicating stdin ...\n" if ( $opt_verbose && ! defined $file); 1356 | 1357 | my $log_ref = slurp_file($file); 1358 | 1359 | print STDERR length($$log_ref)." characters read.\n" if ( $opt_verbose ); 1360 | 1361 | # get the version number 1362 | my $report_version = parse_report_version($log_ref); 1363 | $report_version or die "No crash report version in $file"; 1364 | 1365 | # setup the tool paths we will need 1366 | my $sdkGuess = parse_SDKGuess($log_ref); 1367 | print STDERR "SDK guess for tool search is '$sdkGuess'\n" if $opt_verbose; 1368 | $otool = getToolPath("otool", $sdkGuess); 1369 | $atos = getToolPath("atos", $sdkGuess); 1370 | $symbolstool = getToolPath("symbols", $sdkGuess); 1371 | $size = getToolPath("size", $sdkGuess); 1372 | 1373 | # spindump-based reports will have an "Steps:" line. 1374 | # ReportCrash-based reports will not 1375 | my $steps = parse_steps($log_ref); 1376 | my $is_spindump_report = defined $steps; 1377 | 1378 | my $event_type; 1379 | if ($is_spindump_report) { 1380 | 1381 | # Spindump's format changes depending on the event (microstackshots vs regular spindump) 1382 | $event_type = parse_event_type($log_ref); 1383 | $event_type = $event_type || "manual"; 1384 | 1385 | # Cut off spindump's binary format 1386 | $$log_ref =~ s/Spindump binary format.*$//s; 1387 | } 1388 | 1389 | # extract hardware model 1390 | my $model = parse_HardwareModel($log_ref); 1391 | print STDERR "Hardware Model $model\n" if $opt_verbose; 1392 | 1393 | # extract build 1394 | my ($version, $build) = parse_OSVersion($log_ref); 1395 | print STDERR "OS Version $version Build $build\n" if $opt_verbose; 1396 | 1397 | my @process_sections = parse_processes($log_ref, $is_spindump_report, $event_type); 1398 | 1399 | my $header; 1400 | my $multiple_processes = 0; 1401 | if (scalar(@process_sections) > 1) { 1402 | # If we found multiple process sections, the first section is just the report's header 1403 | $header = shift @process_sections; 1404 | 1405 | print STDERR "Found " . scalar(@process_sections) . " process sections\n" if $opt_verbose; 1406 | $multiple_processes = 1; 1407 | } 1408 | 1409 | my $symbolicated_something = 0; 1410 | 1411 | for my $process_section (@process_sections) { 1412 | if ($multiple_processes) { 1413 | print STDERR "Processing " . ($$process_section =~ /^.*:\s+(.*)/)[0] . "\n"; 1414 | } 1415 | 1416 | 1417 | # read the binary images 1418 | my ($images,$first_bundle) = parse_images($process_section, $report_version, $is_spindump_report); 1419 | 1420 | if ( $opt_verbose ) { 1421 | print STDERR keys(%$images) . " binary images referenced:\n"; 1422 | foreach (keys(%$images)) { 1423 | print STDERR $_; 1424 | print STDERR "\t\t("; 1425 | print STDERR $$images{$_}{path}; 1426 | print STDERR ")\n"; 1427 | } 1428 | print STDERR "\n"; 1429 | } 1430 | 1431 | my $bt = {}; 1432 | my $threads = parse_threads($process_section,event_type=>$event_type); 1433 | print STDERR "Num stacks found: " . scalar(keys %$threads) . "\n" if $opt_verbose; 1434 | for my $thread (values %$threads) { 1435 | # merge all of the frames from all backtraces into one 1436 | # collection 1437 | my $b = parse_backtrace($thread,$images,0,$is_spindump_report); 1438 | @$bt{keys %$b} = values %$b; 1439 | } 1440 | 1441 | my $exception = parse_section($process_section,'Last Exception Backtrace', multiline=>1); 1442 | if (defined $exception) { 1443 | ($process_section, $exception) = fixup_last_exception_backtrace($process_section, $exception, $images); 1444 | #my $e = parse_last_exception_backtrace($exception, $images, 1); 1445 | my $e = parse_backtrace($exception, $images,1,$is_spindump_report); 1446 | 1447 | # treat these frames in the same was as any thread 1448 | @$bt{keys %$e} = values %$e; 1449 | } 1450 | 1451 | # sort out just the images needed for this backtrace 1452 | prune_used_images($images,$bt); 1453 | if ( $opt_verbose ) { 1454 | print STDERR keys(%$images) . " binary images remain after pruning:\n"; 1455 | foreach my $junk (keys(%$images)) { 1456 | print STDERR $junk; 1457 | print STDERR ", "; 1458 | } 1459 | print STDERR "\n"; 1460 | } 1461 | 1462 | @extra_search_paths = (@extra_search_paths, getSymbolDirPaths($model, $version, $build)); 1463 | fetch_symbolled_binaries($images,$build,$first_bundle,@extra_search_paths); 1464 | 1465 | # If we didn't get *any* symbolled binaries, just print out the original crash log. 1466 | my $imageCount = keys(%$images); 1467 | if ($imageCount == 0) { 1468 | next; 1469 | } 1470 | 1471 | # run atos 1472 | symbolize_frames($images,$bt,$is_spindump_report); 1473 | 1474 | if(keys %$bt) { 1475 | # run our fancy regex 1476 | $process_section = replace_symbolized_frames($process_section,$bt,$images,$is_spindump_report); 1477 | 1478 | $symbolicated_something = 1; 1479 | } else { 1480 | # There were no symbols found, don't change the section 1481 | } 1482 | } 1483 | 1484 | if ($symbolicated_something) { 1485 | if (defined $header) { 1486 | output_log($header); 1487 | } 1488 | 1489 | output_log($_) for @process_sections; 1490 | } else { 1491 | #There were no symbols found 1492 | print STDERR "No symbolic information found\n"; 1493 | output_log($log_ref); 1494 | } 1495 | 1496 | } 1497 | -------------------------------------------------------------------------------- /iOSMonkey.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lemonzhulixin/iOS-monkey/f0a25e7cf10c60a8d53cbd8dc0818226cd17fcdc/iOSMonkey.pptx -------------------------------------------------------------------------------- /monkey.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF8 -*- 2 | from iOSCrashAnalysis.CrashExport import CrashExport 3 | from iOSCrashAnalysis.getPakeage import getPakeage 4 | from iOSCrashAnalysis import mysql_monkey 5 | from iOSCrashAnalysis.FileOperate import * 6 | from iOSCrashAnalysis.BaseIosPhone import get_ios_devices,get_ios_PhoneInfo 7 | from iOSCrashAnalysis.FileOperate import FileFilt 8 | 9 | 10 | 11 | PATH = lambda p: os.path.abspath( 12 | os.path.join(os.path.dirname(__file__), p) 13 | ) 14 | 15 | def monkey(devicename): 16 | cmd_monkey = "xcodebuild -project /Users/iOS_Team/.jenkins/workspace/iOS_Monkey_VivaVideo/XCTestWD/XCTestWD/XCTestWD.xcodeproj " \ 17 | "-scheme XCTestWDUITests " \ 18 | "-destination 'platform=iOS,name=" + devicename + "' " + \ 19 | "XCTESTWD_PORT=8001 " + \ 20 | "clean test" 21 | 22 | print(cmd_monkey) 23 | try: 24 | os.system(cmd_monkey) 25 | except Exception as msg: 26 | print('error message:', msg) 27 | raise 28 | 29 | if __name__ == '__main__': 30 | print('获取设备信息') 31 | # dev_list = [] 32 | # devices = get_ios_devices() 33 | # for i in range(len(devices)): 34 | # duid = get_ios_devices()[i] 35 | # dev = get_ios_PhoneInfo(duid) 36 | # dev_list.append(dev) 37 | # print(dev_list) 38 | 39 | deviceName = 'iPhone2140' 40 | deviceID = 'e80251f0e66967f51add3ad0cdc389933715c3ed' 41 | release = '9.3.2' 42 | 43 | print('远程复制ipa文件到本地') 44 | start_time = time.strftime('%Y%m%d%H%M%S', time.localtime()) 45 | cmd_copy = 'sshpass -p ios scp -r iOS_Team@10.0.35.xxx:/Users/iOS_Team/XiaoYing_AutoBuild/XiaoYing/XiaoYingApp/fastlane/output_ipa/ ~/Desktop' 46 | os.system(cmd_copy) 47 | 48 | print('安装ipa测试包到设备') 49 | path = "/Users/iOS_Team/Desktop/output_ipa/" 50 | file_format = ['.ipa'] 51 | ipa_path = getPakeage().get_ipa(path, file_format) 52 | getPakeage().install(path, file_format, deviceID) 53 | 54 | print("启动monkey") 55 | monkey(deviceName) 56 | 57 | print('解析crash report') 58 | find_str = 'XiaoYing-' # 待测app crashreport文件关键字 59 | file_format1 = [".ips"] # 导出的crash文件后缀 60 | file_format2 = [".crash"] # 解析后的crash文件后缀 61 | CrashExport(deviceID, find_str, file_format1, file_format2) 62 | end_time = time.strftime('%Y%m%d%H%M%S', time.localtime()) 63 | 64 | print('测试结果数据解析并DB存储') 65 | loacl_time = time.strftime('%Y%m%d%H%M%S', time.localtime()) 66 | iOS_tag = 'iOS_' + loacl_time 67 | 68 | print('插入数据到device表') 69 | deviceData = { 70 | 'name': deviceName, 71 | 'serial_number': deviceID, 72 | 'version': release, 73 | 'status': 1, 74 | 'tag': 'iOS' 75 | } 76 | 77 | print('插入数据到apk信息表') 78 | # ipa_path = '/Users/zhulixin/Desktop/output_ipa/day_inke_release_xiaoying.ipa' 79 | ipainfo = getPakeage().getIpaInfo(ipa_path) 80 | apkData = { 81 | 'app_name': ipainfo[0], 82 | 'ver_name': ipainfo[2], 83 | 'ver_code': ipainfo[3], 84 | 'file_name': 'day_inke_release_xiaoying.ipa', 85 | 'file_path': ipa_path, 86 | 'build_time': start_time, 87 | 'tag': iOS_tag 88 | } 89 | 90 | print('插入数据到task表') 91 | taskData = { 92 | 'start_time': start_time, 93 | 'end_time': end_time, 94 | 'app_name': ipainfo[0], 95 | 'devices': 1, 96 | 'test_count': None, 97 | 'pass_count': None, 98 | 'fail_count': None, 99 | 'passing_rate': None, 100 | 'tag': iOS_tag, 101 | 'info': None 102 | } 103 | 104 | print('插入数据到results表') 105 | # f = FileFilt() 106 | # f.FindFile(find_str, file_format1, './CrashInfo/') 107 | # crash_count = len(f.fileList) 108 | # result = 1 109 | # if crash_count: 110 | # result = 0 111 | 112 | resultData = { 113 | 'result_id': start_time + '-monkey-' + ipainfo[0], 114 | 'start_time': start_time, 115 | 'end_time': end_time, 116 | 'device_name': deviceName, 117 | 'apk_id': None, 118 | 'result': None, 119 | 'status': None, 120 | 'CRASHs': None, 121 | 'ANRs': None, 122 | 'tag': iOS_tag, 123 | 'device_log':None, 124 | 'monkey_log': None, 125 | 'monkey_loop': None, 126 | 'cmd':None, 127 | 'seed': None 128 | } 129 | 130 | # print('deviceData:', deviceData) 131 | # mysql_monkey.insert_record_to_phones(deviceData) 132 | 133 | print('apkData:', apkData) 134 | mysql_monkey.insert_record_to_apks(apkData) 135 | 136 | print('taskData:', taskData) 137 | mysql_monkey.insert_record_to_tasks(taskData) 138 | 139 | print('resultData:', resultData) 140 | mysql_monkey.insert_record_to_results(resultData) 141 | 142 | print("压缩测试结果并传") 143 | f = FileFilt() 144 | results_file = f.zip_report(loacl_time, './CrashInfo/', './Results_ZIP/') 145 | url = 'http://10.0.32.xxx:5100/api/v1/iOS-monkey' 146 | files = {'file': open(results_file, 'rb')} 147 | response = requests.post(url, files=files) 148 | json = response.json() 149 | 150 | print("删除本次的测试结果") 151 | f.DelFolder('./CrashInfo/') 152 | print("xxxxxxxxxxxxxxxxxxxxxxxxx Finish Test xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") 153 | --------------------------------------------------------------------------------