├── .gitignore ├── README.md ├── biplist ├── __init__.py └── six.py ├── index.html └── symbol_server.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Installer logs 7 | pip-log.txt 8 | 9 | # Translations 10 | *.mo 11 | 12 | # OS generated files 13 | .DS_Store 14 | .DS_Store? 15 | .Trashes 16 | Thumbs.db 17 | 18 | # Symbols 19 | symbols 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Umeng Crash Symbolicator 2 | ================== 3 | 4 | 一键把友盟iOS崩溃日志里的?????转成可见的symbol。 5 | 6 | [友盟](http://www.umeng.com/)的iOS应用错误分析工具收集到的崩溃日志(Stack Trace),有些情况下无法显示出正确的symbol,显示为一堆问号?????。 7 | 使用此工具可以在浏览器中一键把问号转换成symbol和代码位置。 8 | 9 | 10 | 运行环境 11 | -- 12 | 13 | 1. Python 2.7(其他版本没试过) 14 | 15 | 2. 安装[Tornado Web Server](http://www.tornadoweb.org) 16 | 17 | 18 | 使用方法 19 | -- 20 | 21 | 1. 把不同版本的App Bundle和对应的dSYM放到`symbols`目录下。目录结构如下: 22 | 23 | + symbols 24 | + DemoApp-1.0 25 | - DemoApp.app 26 | - DemoApp.app.dSYM 27 | + DemoApp-1.1 28 | - DemoApp.app 29 | - DemoApp.app.dSYM 30 | 31 | 这些App Bundle和dSYM必须是提交App Store的版本,一般可以从XCode的Archives里找到。 32 | 在Orgnizer - Archives里右键单击一个archive,选择Show in Finder, 33 | 在`*.xcarchive`上右键单击,选择Show Package Contents,把目录下的*.app和*.dSYM拷到同一目录中即可。 34 | 35 | 2. 启动服务。地址和端口可以自定义: 36 | 37 | python symbol_server.py localhost 8000 38 | 39 | 启动成功后显示 40 | 41 | Server started - http://localhost:8000 42 | 43 | 3. 用浏览器里访问上一步提示的地址,把链接`Umeng Crash Symbolicator`拖到书签栏上。 44 | 45 | 4. 在使用友盟的错误分析工具页面时,点击书签栏里的`Umeng Crash Symbolicator`即可把问号转换成symbol。 46 | 47 | 48 | 原理 49 | -- 50 | 51 | 友盟的崩溃日志中记录了`dSYM UDID`,通过UDID查询到该条崩溃日志对应的App版本。[[参考1]][ref1] 52 | 再取Stack Trace里问号前面的地址,结合`Slide Address`和`Base Address`,使用`atos`命令查出symbol。[[参考2]][ref2] [[参考3]][ref3] 53 | 54 | [ref1]: http://draftdog.blogspot.com/2012/04/figuring-out-uuid-for-dsym.html 55 | [ref2]: http://stackoverflow.com/questions/1460892/symbolicating-iphone-app-crash-reports 56 | [ref3]: http://stackoverflow.com/questions/13574933/ios-crash-reports-atos-not-working-as-expected/13576028#13576028 57 | 58 | 示例 59 | -- 60 | 61 | 显示错误的Stack Trace像这样: 62 | 63 | *** -[NSDictionary initWithDictionary:copyItems:]: dictionary argument is not an NSDictionary 64 | (null) 65 | ( 66 | 0 CoreFoundation 0x328632bb + 186 67 | 1 libobjc.A.dylib 0x3a70e97f objc_exception_throw + 30 68 | 2 CoreFoundation 0x327efff5 + 212 69 | 3 CoreFoundation 0x327eff0b + 42 70 | 4 ???????????? 0x0009ff8f ???????????? + 233359 71 | 5 ???????????? 0x0009fefb ???????????? + 233211 72 | 6 ???????????? 0x001625cd ???????????? + 1029581 73 | 7 ???????????? 0x00163eb1 ???????????? + 1035953 74 | 8 Foundation 0x330f8d41 + 200 75 | 9 Foundation 0x330f05c1 + 840 76 | ) 77 | 78 | 使用此工具转换后: 79 | 80 | *** -[NSDictionary initWithDictionary:copyItems:]: dictionary argument is not an NSDictionary 81 | (null) 82 | ( 83 | 0 CoreFoundation 0x328632bb + 186 84 | 1 libobjc.A.dylib 0x3a70e97f objc_exception_throw + 30 85 | 2 CoreFoundation 0x327efff5 + 212 86 | 3 CoreFoundation 0x327eff0b + 42 87 | 4 ???????????? 0x0009ff8f -[BookManager getNoteSummaryDictWithBookID:] (BookManager.m:1784) 88 | + 233359 89 | 5 ???????????? 0x0009fefb -[BookManager getNoteSummaryList] (BookManager.m:1775) 90 | + 233211 91 | 6 ???????????? 0x001625cd +[DkAppURLProtocol getJsonForAllNoteSummaryWithSortType:] (DkAppURLProtocol.m:220) 92 | + 1029581 93 | 7 ???????????? 0x00163eb1 -[DkAppURLProtocol startLoading] (DkAppURLProtocol.m:495) 94 | + 1035953 95 | 8 Foundation 0x330f8d41 + 200 96 | 9 Foundation 0x330f05c1 + 840 97 | -------------------------------------------------------------------------------- /biplist/__init__.py: -------------------------------------------------------------------------------- 1 | """biplist -- a library for reading and writing binary property list files. 2 | 3 | Binary Property List (plist) files provide a faster and smaller serialization 4 | format for property lists on OS X. This is a library for generating binary 5 | plists which can be read by OS X, iOS, or other clients. 6 | 7 | The API models the plistlib API, and will call through to plistlib when 8 | XML serialization or deserialization is required. 9 | 10 | To generate plists with UID values, wrap the values with the Uid object. The 11 | value must be an int. 12 | 13 | To generate plists with NSData/CFData values, wrap the values with the 14 | Data object. The value must be a string. 15 | 16 | Date values can only be datetime.datetime objects. 17 | 18 | The exceptions InvalidPlistException and NotBinaryPlistException may be 19 | thrown to indicate that the data cannot be serialized or deserialized as 20 | a binary plist. 21 | 22 | Plist generation example: 23 | 24 | from biplist import * 25 | from datetime import datetime 26 | plist = {'aKey':'aValue', 27 | '0':1.322, 28 | 'now':datetime.now(), 29 | 'list':[1,2,3], 30 | 'tuple':('a','b','c') 31 | } 32 | try: 33 | writePlist(plist, "example.plist") 34 | except (InvalidPlistException, NotBinaryPlistException), e: 35 | print "Something bad happened:", e 36 | 37 | Plist parsing example: 38 | 39 | from biplist import * 40 | try: 41 | plist = readPlist("example.plist") 42 | print plist 43 | except (InvalidPlistException, NotBinaryPlistException), e: 44 | print "Not a plist:", e 45 | """ 46 | 47 | import sys 48 | from collections import namedtuple 49 | import calendar 50 | import datetime 51 | import math 52 | import plistlib 53 | from struct import pack, unpack 54 | import sys 55 | import time 56 | 57 | import six 58 | 59 | __all__ = [ 60 | 'Uid', 'Data', 'readPlist', 'writePlist', 'readPlistFromString', 61 | 'writePlistToString', 'InvalidPlistException', 'NotBinaryPlistException' 62 | ] 63 | 64 | apple_reference_date_offset = 978307200 65 | 66 | class Uid(int): 67 | """Wrapper around integers for representing UID values. This 68 | is used in keyed archiving.""" 69 | def __repr__(self): 70 | return "Uid(%d)" % self 71 | 72 | class Data(six.binary_type): 73 | """Wrapper around str types for representing Data values.""" 74 | pass 75 | 76 | class InvalidPlistException(Exception): 77 | """Raised when the plist is incorrectly formatted.""" 78 | pass 79 | 80 | class NotBinaryPlistException(Exception): 81 | """Raised when a binary plist was expected but not encountered.""" 82 | pass 83 | 84 | def readPlist(pathOrFile): 85 | """Raises NotBinaryPlistException, InvalidPlistException""" 86 | didOpen = False 87 | result = None 88 | if isinstance(pathOrFile, (six.binary_type, six.text_type)): 89 | pathOrFile = open(pathOrFile, 'rb') 90 | didOpen = True 91 | try: 92 | reader = PlistReader(pathOrFile) 93 | result = reader.parse() 94 | except NotBinaryPlistException as e: 95 | try: 96 | pathOrFile.seek(0) 97 | result = plistlib.readPlist(pathOrFile) 98 | result = wrapDataObject(result, for_binary=True) 99 | except Exception as e: 100 | raise InvalidPlistException(e) 101 | if didOpen: 102 | pathOrFile.close() 103 | return result 104 | 105 | def wrapDataObject(o, for_binary=False): 106 | if isinstance(o, Data) and not for_binary: 107 | o = plistlib.Data(o) 108 | elif isinstance(o, plistlib.Data) and for_binary: 109 | o = Data(o.data) 110 | elif isinstance(o, tuple): 111 | o = wrapDataObject(list(o), for_binary) 112 | o = tuple(o) 113 | elif isinstance(o, list): 114 | for i in range(len(o)): 115 | o[i] = wrapDataObject(o[i], for_binary) 116 | elif isinstance(o, dict): 117 | for k in o: 118 | o[k] = wrapDataObject(o[k], for_binary) 119 | return o 120 | 121 | def writePlist(rootObject, pathOrFile, binary=True): 122 | if not binary: 123 | rootObject = wrapDataObject(rootObject, binary) 124 | return plistlib.writePlist(rootObject, pathOrFile) 125 | else: 126 | didOpen = False 127 | if isinstance(pathOrFile, (six.binary_type, six.text_type)): 128 | pathOrFile = open(pathOrFile, 'wb') 129 | didOpen = True 130 | writer = PlistWriter(pathOrFile) 131 | result = writer.writeRoot(rootObject) 132 | if didOpen: 133 | pathOrFile.close() 134 | return result 135 | 136 | def readPlistFromString(data): 137 | return readPlist(six.BytesIO(data)) 138 | 139 | def writePlistToString(rootObject, binary=True): 140 | if not binary: 141 | rootObject = wrapDataObject(rootObject, binary) 142 | if six.PY3: 143 | return plistlib.writePlistToBytes(rootObject) 144 | else: 145 | return plistlib.writePlistToString(rootObject) 146 | else: 147 | io = six.BytesIO() 148 | writer = PlistWriter(io) 149 | writer.writeRoot(rootObject) 150 | return io.getvalue() 151 | 152 | def is_stream_binary_plist(stream): 153 | stream.seek(0) 154 | header = stream.read(7) 155 | if header == six.b('bplist0'): 156 | return True 157 | else: 158 | return False 159 | 160 | PlistTrailer = namedtuple('PlistTrailer', 'offsetSize, objectRefSize, offsetCount, topLevelObjectNumber, offsetTableOffset') 161 | PlistByteCounts = namedtuple('PlistByteCounts', 'nullBytes, boolBytes, intBytes, realBytes, dateBytes, dataBytes, stringBytes, uidBytes, arrayBytes, setBytes, dictBytes') 162 | 163 | class PlistReader(object): 164 | file = None 165 | contents = '' 166 | offsets = None 167 | trailer = None 168 | currentOffset = 0 169 | 170 | def __init__(self, fileOrStream): 171 | """Raises NotBinaryPlistException.""" 172 | self.reset() 173 | self.file = fileOrStream 174 | 175 | def parse(self): 176 | return self.readRoot() 177 | 178 | def reset(self): 179 | self.trailer = None 180 | self.contents = '' 181 | self.offsets = [] 182 | self.currentOffset = 0 183 | 184 | def readRoot(self): 185 | result = None 186 | self.reset() 187 | # Get the header, make sure it's a valid file. 188 | if not is_stream_binary_plist(self.file): 189 | raise NotBinaryPlistException() 190 | self.file.seek(0) 191 | self.contents = self.file.read() 192 | if len(self.contents) < 32: 193 | raise InvalidPlistException("File is too short.") 194 | trailerContents = self.contents[-32:] 195 | try: 196 | self.trailer = PlistTrailer._make(unpack("!xxxxxxBBQQQ", trailerContents)) 197 | offset_size = self.trailer.offsetSize * self.trailer.offsetCount 198 | offset = self.trailer.offsetTableOffset 199 | offset_contents = self.contents[offset:offset+offset_size] 200 | offset_i = 0 201 | while offset_i < self.trailer.offsetCount: 202 | begin = self.trailer.offsetSize*offset_i 203 | tmp_contents = offset_contents[begin:begin+self.trailer.offsetSize] 204 | tmp_sized = self.getSizedInteger(tmp_contents, self.trailer.offsetSize) 205 | self.offsets.append(tmp_sized) 206 | offset_i += 1 207 | self.setCurrentOffsetToObjectNumber(self.trailer.topLevelObjectNumber) 208 | result = self.readObject() 209 | except TypeError as e: 210 | raise InvalidPlistException(e) 211 | return result 212 | 213 | def setCurrentOffsetToObjectNumber(self, objectNumber): 214 | self.currentOffset = self.offsets[objectNumber] 215 | 216 | def readObject(self): 217 | result = None 218 | tmp_byte = self.contents[self.currentOffset:self.currentOffset+1] 219 | marker_byte = unpack("!B", tmp_byte)[0] 220 | format = (marker_byte >> 4) & 0x0f 221 | extra = marker_byte & 0x0f 222 | self.currentOffset += 1 223 | 224 | def proc_extra(extra): 225 | if extra == 0b1111: 226 | #self.currentOffset += 1 227 | extra = self.readObject() 228 | return extra 229 | 230 | # bool, null, or fill byte 231 | if format == 0b0000: 232 | if extra == 0b0000: 233 | result = None 234 | elif extra == 0b1000: 235 | result = False 236 | elif extra == 0b1001: 237 | result = True 238 | elif extra == 0b1111: 239 | pass # fill byte 240 | else: 241 | raise InvalidPlistException("Invalid object found at offset: %d" % (self.currentOffset - 1)) 242 | # int 243 | elif format == 0b0001: 244 | extra = proc_extra(extra) 245 | result = self.readInteger(pow(2, extra)) 246 | # real 247 | elif format == 0b0010: 248 | extra = proc_extra(extra) 249 | result = self.readReal(extra) 250 | # date 251 | elif format == 0b0011 and extra == 0b0011: 252 | result = self.readDate() 253 | # data 254 | elif format == 0b0100: 255 | extra = proc_extra(extra) 256 | result = self.readData(extra) 257 | # ascii string 258 | elif format == 0b0101: 259 | extra = proc_extra(extra) 260 | result = self.readAsciiString(extra) 261 | # Unicode string 262 | elif format == 0b0110: 263 | extra = proc_extra(extra) 264 | result = self.readUnicode(extra) 265 | # uid 266 | elif format == 0b1000: 267 | result = self.readUid(extra) 268 | # array 269 | elif format == 0b1010: 270 | extra = proc_extra(extra) 271 | result = self.readArray(extra) 272 | # set 273 | elif format == 0b1100: 274 | extra = proc_extra(extra) 275 | result = set(self.readArray(extra)) 276 | # dict 277 | elif format == 0b1101: 278 | extra = proc_extra(extra) 279 | result = self.readDict(extra) 280 | else: 281 | raise InvalidPlistException("Invalid object found: {format: %s, extra: %s}" % (bin(format), bin(extra))) 282 | return result 283 | 284 | def readInteger(self, bytes): 285 | result = 0 286 | original_offset = self.currentOffset 287 | data = self.contents[self.currentOffset:self.currentOffset+bytes] 288 | result = self.getSizedInteger(data, bytes) 289 | self.currentOffset = original_offset + bytes 290 | return result 291 | 292 | def readReal(self, length): 293 | result = 0.0 294 | to_read = pow(2, length) 295 | data = self.contents[self.currentOffset:self.currentOffset+to_read] 296 | if length == 2: # 4 bytes 297 | result = unpack('>f', data)[0] 298 | elif length == 3: # 8 bytes 299 | result = unpack('>d', data)[0] 300 | else: 301 | raise InvalidPlistException("Unknown real of length %d bytes" % to_read) 302 | return result 303 | 304 | def readRefs(self, count): 305 | refs = [] 306 | i = 0 307 | while i < count: 308 | fragment = self.contents[self.currentOffset:self.currentOffset+self.trailer.objectRefSize] 309 | ref = self.getSizedInteger(fragment, len(fragment)) 310 | refs.append(ref) 311 | self.currentOffset += self.trailer.objectRefSize 312 | i += 1 313 | return refs 314 | 315 | def readArray(self, count): 316 | result = [] 317 | values = self.readRefs(count) 318 | i = 0 319 | while i < len(values): 320 | self.setCurrentOffsetToObjectNumber(values[i]) 321 | value = self.readObject() 322 | result.append(value) 323 | i += 1 324 | return result 325 | 326 | def readDict(self, count): 327 | result = {} 328 | keys = self.readRefs(count) 329 | values = self.readRefs(count) 330 | i = 0 331 | while i < len(keys): 332 | self.setCurrentOffsetToObjectNumber(keys[i]) 333 | key = self.readObject() 334 | self.setCurrentOffsetToObjectNumber(values[i]) 335 | value = self.readObject() 336 | result[key] = value 337 | i += 1 338 | return result 339 | 340 | def readAsciiString(self, length): 341 | result = unpack("!%ds" % length, self.contents[self.currentOffset:self.currentOffset+length])[0] 342 | self.currentOffset += length 343 | return result 344 | 345 | def readUnicode(self, length): 346 | actual_length = length*2 347 | data = self.contents[self.currentOffset:self.currentOffset+actual_length] 348 | # unpack not needed?!! data = unpack(">%ds" % (actual_length), data)[0] 349 | self.currentOffset += actual_length 350 | return data.decode('utf_16_be') 351 | 352 | def readDate(self): 353 | global apple_reference_date_offset 354 | result = unpack(">d", self.contents[self.currentOffset:self.currentOffset+8])[0] 355 | result = datetime.datetime.utcfromtimestamp(result + apple_reference_date_offset) 356 | self.currentOffset += 8 357 | return result 358 | 359 | def readData(self, length): 360 | result = self.contents[self.currentOffset:self.currentOffset+length] 361 | self.currentOffset += length 362 | return Data(result) 363 | 364 | def readUid(self, length): 365 | return Uid(self.readInteger(length+1)) 366 | 367 | def getSizedInteger(self, data, bytes): 368 | result = 0 369 | # 1, 2, and 4 byte integers are unsigned 370 | if bytes == 1: 371 | result = unpack('>B', data)[0] 372 | elif bytes == 2: 373 | result = unpack('>H', data)[0] 374 | elif bytes == 4: 375 | result = unpack('>L', data)[0] 376 | elif bytes == 8: 377 | result = unpack('>q', data)[0] 378 | else: 379 | raise InvalidPlistException("Encountered integer longer than 8 bytes.") 380 | return result 381 | 382 | class HashableWrapper(object): 383 | def __init__(self, value): 384 | self.value = value 385 | def __repr__(self): 386 | return "" % [self.value] 387 | 388 | class BoolWrapper(object): 389 | def __init__(self, value): 390 | self.value = value 391 | def __repr__(self): 392 | return "" % self.value 393 | 394 | class PlistWriter(object): 395 | header = six.b('bplist00bybiplist1.0') 396 | file = None 397 | byteCounts = None 398 | trailer = None 399 | computedUniques = None 400 | writtenReferences = None 401 | referencePositions = None 402 | wrappedTrue = None 403 | wrappedFalse = None 404 | 405 | def __init__(self, file): 406 | self.reset() 407 | self.file = file 408 | self.wrappedTrue = BoolWrapper(True) 409 | self.wrappedFalse = BoolWrapper(False) 410 | 411 | def reset(self): 412 | self.byteCounts = PlistByteCounts(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) 413 | self.trailer = PlistTrailer(0, 0, 0, 0, 0) 414 | 415 | # A set of all the uniques which have been computed. 416 | self.computedUniques = set() 417 | # A list of all the uniques which have been written. 418 | self.writtenReferences = {} 419 | # A dict of the positions of the written uniques. 420 | self.referencePositions = {} 421 | 422 | def positionOfObjectReference(self, obj): 423 | """If the given object has been written already, return its 424 | position in the offset table. Otherwise, return None.""" 425 | return self.writtenReferences.get(obj) 426 | 427 | def writeRoot(self, root): 428 | """ 429 | Strategy is: 430 | - write header 431 | - wrap root object so everything is hashable 432 | - compute size of objects which will be written 433 | - need to do this in order to know how large the object refs 434 | will be in the list/dict/set reference lists 435 | - write objects 436 | - keep objects in writtenReferences 437 | - keep positions of object references in referencePositions 438 | - write object references with the length computed previously 439 | - computer object reference length 440 | - write object reference positions 441 | - write trailer 442 | """ 443 | output = self.header 444 | wrapped_root = self.wrapRoot(root) 445 | should_reference_root = True#not isinstance(wrapped_root, HashableWrapper) 446 | self.computeOffsets(wrapped_root, asReference=should_reference_root, isRoot=True) 447 | self.trailer = self.trailer._replace(**{'objectRefSize':self.intSize(len(self.computedUniques))}) 448 | (_, output) = self.writeObjectReference(wrapped_root, output) 449 | output = self.writeObject(wrapped_root, output, setReferencePosition=True) 450 | 451 | # output size at this point is an upper bound on how big the 452 | # object reference offsets need to be. 453 | self.trailer = self.trailer._replace(**{ 454 | 'offsetSize':self.intSize(len(output)), 455 | 'offsetCount':len(self.computedUniques), 456 | 'offsetTableOffset':len(output), 457 | 'topLevelObjectNumber':0 458 | }) 459 | 460 | output = self.writeOffsetTable(output) 461 | output += pack('!xxxxxxBBQQQ', *self.trailer) 462 | self.file.write(output) 463 | 464 | def wrapRoot(self, root): 465 | if isinstance(root, bool): 466 | if root is True: 467 | return self.wrappedTrue 468 | else: 469 | return self.wrappedFalse 470 | elif isinstance(root, set): 471 | n = set() 472 | for value in root: 473 | n.add(self.wrapRoot(value)) 474 | return HashableWrapper(n) 475 | elif isinstance(root, dict): 476 | n = {} 477 | for key, value in six.iteritems(root): 478 | n[self.wrapRoot(key)] = self.wrapRoot(value) 479 | return HashableWrapper(n) 480 | elif isinstance(root, list): 481 | n = [] 482 | for value in root: 483 | n.append(self.wrapRoot(value)) 484 | return HashableWrapper(n) 485 | elif isinstance(root, tuple): 486 | n = tuple([self.wrapRoot(value) for value in root]) 487 | return HashableWrapper(n) 488 | else: 489 | return root 490 | 491 | def incrementByteCount(self, field, incr=1): 492 | self.byteCounts = self.byteCounts._replace(**{field:self.byteCounts.__getattribute__(field) + incr}) 493 | 494 | def computeOffsets(self, obj, asReference=False, isRoot=False): 495 | def check_key(key): 496 | if key is None: 497 | raise InvalidPlistException('Dictionary keys cannot be null in plists.') 498 | elif isinstance(key, Data): 499 | raise InvalidPlistException('Data cannot be dictionary keys in plists.') 500 | elif not isinstance(key, (six.binary_type, six.text_type)): 501 | raise InvalidPlistException('Keys must be strings.') 502 | 503 | def proc_size(size): 504 | if size > 0b1110: 505 | size += self.intSize(size) 506 | return size 507 | # If this should be a reference, then we keep a record of it in the 508 | # uniques table. 509 | if asReference: 510 | if obj in self.computedUniques: 511 | return 512 | else: 513 | self.computedUniques.add(obj) 514 | 515 | if obj is None: 516 | self.incrementByteCount('nullBytes') 517 | elif isinstance(obj, BoolWrapper): 518 | self.incrementByteCount('boolBytes') 519 | elif isinstance(obj, Uid): 520 | size = self.intSize(obj) 521 | self.incrementByteCount('uidBytes', incr=1+size) 522 | elif isinstance(obj, six.integer_types): 523 | size = self.intSize(obj) 524 | self.incrementByteCount('intBytes', incr=1+size) 525 | elif isinstance(obj, (float)): 526 | size = self.realSize(obj) 527 | self.incrementByteCount('realBytes', incr=1+size) 528 | elif isinstance(obj, datetime.datetime): 529 | self.incrementByteCount('dateBytes', incr=2) 530 | elif isinstance(obj, Data): 531 | size = proc_size(len(obj)) 532 | self.incrementByteCount('dataBytes', incr=1+size) 533 | elif isinstance(obj, (six.text_type, six.binary_type)): 534 | size = proc_size(len(obj)) 535 | self.incrementByteCount('stringBytes', incr=1+size) 536 | elif isinstance(obj, HashableWrapper): 537 | obj = obj.value 538 | if isinstance(obj, set): 539 | size = proc_size(len(obj)) 540 | self.incrementByteCount('setBytes', incr=1+size) 541 | for value in obj: 542 | self.computeOffsets(value, asReference=True) 543 | elif isinstance(obj, (list, tuple)): 544 | size = proc_size(len(obj)) 545 | self.incrementByteCount('arrayBytes', incr=1+size) 546 | for value in obj: 547 | asRef = True 548 | self.computeOffsets(value, asReference=True) 549 | elif isinstance(obj, dict): 550 | size = proc_size(len(obj)) 551 | self.incrementByteCount('dictBytes', incr=1+size) 552 | for key, value in six.iteritems(obj): 553 | check_key(key) 554 | self.computeOffsets(key, asReference=True) 555 | self.computeOffsets(value, asReference=True) 556 | else: 557 | raise InvalidPlistException("Unknown object type.") 558 | 559 | def writeObjectReference(self, obj, output): 560 | """Tries to write an object reference, adding it to the references 561 | table. Does not write the actual object bytes or set the reference 562 | position. Returns a tuple of whether the object was a new reference 563 | (True if it was, False if it already was in the reference table) 564 | and the new output. 565 | """ 566 | position = self.positionOfObjectReference(obj) 567 | if position is None: 568 | self.writtenReferences[obj] = len(self.writtenReferences) 569 | output += self.binaryInt(len(self.writtenReferences) - 1, bytes=self.trailer.objectRefSize) 570 | return (True, output) 571 | else: 572 | output += self.binaryInt(position, bytes=self.trailer.objectRefSize) 573 | return (False, output) 574 | 575 | def writeObject(self, obj, output, setReferencePosition=False): 576 | """Serializes the given object to the output. Returns output. 577 | If setReferencePosition is True, will set the position the 578 | object was written. 579 | """ 580 | def proc_variable_length(format, length): 581 | result = six.b('') 582 | if length > 0b1110: 583 | result += pack('!B', (format << 4) | 0b1111) 584 | result = self.writeObject(length, result) 585 | else: 586 | result += pack('!B', (format << 4) | length) 587 | return result 588 | 589 | if isinstance(obj, six.text_type) and obj == six.u(''): 590 | # The Apple Plist decoder can't decode a zero length Unicode string. 591 | obj = six.b('') 592 | 593 | if setReferencePosition: 594 | self.referencePositions[obj] = len(output) 595 | 596 | if obj is None: 597 | output += pack('!B', 0b00000000) 598 | elif isinstance(obj, BoolWrapper): 599 | if obj.value is False: 600 | output += pack('!B', 0b00001000) 601 | else: 602 | output += pack('!B', 0b00001001) 603 | elif isinstance(obj, Uid): 604 | size = self.intSize(obj) 605 | output += pack('!B', (0b1000 << 4) | size - 1) 606 | output += self.binaryInt(obj) 607 | elif isinstance(obj, six.integer_types): 608 | bytes = self.intSize(obj) 609 | root = math.log(bytes, 2) 610 | output += pack('!B', (0b0001 << 4) | int(root)) 611 | output += self.binaryInt(obj) 612 | elif isinstance(obj, float): 613 | # just use doubles 614 | output += pack('!B', (0b0010 << 4) | 3) 615 | output += self.binaryReal(obj) 616 | elif isinstance(obj, datetime.datetime): 617 | timestamp = calendar.timegm(obj.utctimetuple()) 618 | timestamp -= apple_reference_date_offset 619 | output += pack('!B', 0b00110011) 620 | output += pack('!d', float(timestamp)) 621 | elif isinstance(obj, Data): 622 | output += proc_variable_length(0b0100, len(obj)) 623 | output += obj 624 | elif isinstance(obj, six.text_type): 625 | bytes = obj.encode('utf_16_be') 626 | output += proc_variable_length(0b0110, len(bytes)//2) 627 | output += bytes 628 | elif isinstance(obj, six.binary_type): 629 | bytes = obj 630 | output += proc_variable_length(0b0101, len(bytes)) 631 | output += bytes 632 | elif isinstance(obj, HashableWrapper): 633 | obj = obj.value 634 | if isinstance(obj, (set, list, tuple)): 635 | if isinstance(obj, set): 636 | output += proc_variable_length(0b1100, len(obj)) 637 | else: 638 | output += proc_variable_length(0b1010, len(obj)) 639 | 640 | objectsToWrite = [] 641 | for objRef in obj: 642 | (isNew, output) = self.writeObjectReference(objRef, output) 643 | if isNew: 644 | objectsToWrite.append(objRef) 645 | for objRef in objectsToWrite: 646 | output = self.writeObject(objRef, output, setReferencePosition=True) 647 | elif isinstance(obj, dict): 648 | output += proc_variable_length(0b1101, len(obj)) 649 | keys = [] 650 | values = [] 651 | objectsToWrite = [] 652 | for key, value in six.iteritems(obj): 653 | keys.append(key) 654 | values.append(value) 655 | for key in keys: 656 | (isNew, output) = self.writeObjectReference(key, output) 657 | if isNew: 658 | objectsToWrite.append(key) 659 | for value in values: 660 | (isNew, output) = self.writeObjectReference(value, output) 661 | if isNew: 662 | objectsToWrite.append(value) 663 | for objRef in objectsToWrite: 664 | output = self.writeObject(objRef, output, setReferencePosition=True) 665 | return output 666 | 667 | def writeOffsetTable(self, output): 668 | """Writes all of the object reference offsets.""" 669 | all_positions = [] 670 | writtenReferences = list(self.writtenReferences.items()) 671 | writtenReferences.sort(key=lambda x: x[1]) 672 | for obj,order in writtenReferences: 673 | # Porting note: Elsewhere we deliberately replace empty unicdoe strings 674 | # with empty binary strings, but the empty unicode string 675 | # goes into writtenReferences. This isn't an issue in Py2 676 | # because u'' and b'' have the same hash; but it is in 677 | # Py3, where they don't. 678 | if six.PY3 and obj == six.u(''): 679 | obj = six.b('') 680 | position = self.referencePositions.get(obj) 681 | if position is None: 682 | raise InvalidPlistException("Error while writing offsets table. Object not found. %s" % obj) 683 | output += self.binaryInt(position, self.trailer.offsetSize) 684 | all_positions.append(position) 685 | return output 686 | 687 | def binaryReal(self, obj): 688 | # just use doubles 689 | result = pack('>d', obj) 690 | return result 691 | 692 | def binaryInt(self, obj, bytes=None): 693 | result = six.b('') 694 | if bytes is None: 695 | bytes = self.intSize(obj) 696 | if bytes == 1: 697 | result += pack('>B', obj) 698 | elif bytes == 2: 699 | result += pack('>H', obj) 700 | elif bytes == 4: 701 | result += pack('>L', obj) 702 | elif bytes == 8: 703 | result += pack('>q', obj) 704 | else: 705 | raise InvalidPlistException("Core Foundation can't handle integers with size greater than 8 bytes.") 706 | return result 707 | 708 | def intSize(self, obj): 709 | """Returns the number of bytes necessary to store the given integer.""" 710 | # SIGNED 711 | if obj < 0: # Signed integer, always 8 bytes 712 | return 8 713 | # UNSIGNED 714 | elif obj <= 0xFF: # 1 byte 715 | return 1 716 | elif obj <= 0xFFFF: # 2 bytes 717 | return 2 718 | elif obj <= 0xFFFFFFFF: # 4 bytes 719 | return 4 720 | # SIGNED 721 | # 0x7FFFFFFFFFFFFFFF is the max. 722 | elif obj <= 0x7FFFFFFFFFFFFFFF: # 8 bytes 723 | return 8 724 | else: 725 | raise InvalidPlistException("Core Foundation can't handle integers with size greater than 8 bytes.") 726 | 727 | def realSize(self, obj): 728 | return 8 729 | -------------------------------------------------------------------------------- /biplist/six.py: -------------------------------------------------------------------------------- 1 | """Utilities for writing code that runs on Python 2 and 3""" 2 | 3 | # Copyright (c) 2010-2013 Benjamin Peterson 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | # this software and associated documentation files (the "Software"), to deal in 7 | # the Software without restriction, including without limitation the rights to 8 | # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | # the Software, and to permit persons to whom the Software is furnished to do so, 10 | # subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | import operator 23 | import sys 24 | import types 25 | 26 | __author__ = "Benjamin Peterson " 27 | __version__ = "1.3.0" 28 | 29 | 30 | # True if we are running on Python 3. 31 | PY3 = sys.version_info[0] == 3 32 | 33 | if PY3: 34 | string_types = str, 35 | integer_types = int, 36 | class_types = type, 37 | text_type = str 38 | binary_type = bytes 39 | 40 | MAXSIZE = sys.maxsize 41 | else: 42 | string_types = basestring, 43 | integer_types = (int, long) 44 | class_types = (type, types.ClassType) 45 | text_type = unicode 46 | binary_type = str 47 | 48 | if sys.platform.startswith("java"): 49 | # Jython always uses 32 bits. 50 | MAXSIZE = int((1 << 31) - 1) 51 | else: 52 | # It's possible to have sizeof(long) != sizeof(Py_ssize_t). 53 | class X(object): 54 | def __len__(self): 55 | return 1 << 31 56 | try: 57 | len(X()) 58 | except OverflowError: 59 | # 32-bit 60 | MAXSIZE = int((1 << 31) - 1) 61 | else: 62 | # 64-bit 63 | MAXSIZE = int((1 << 63) - 1) 64 | del X 65 | 66 | 67 | def _add_doc(func, doc): 68 | """Add documentation to a function.""" 69 | func.__doc__ = doc 70 | 71 | 72 | def _import_module(name): 73 | """Import module, returning the module after the last dot.""" 74 | __import__(name) 75 | return sys.modules[name] 76 | 77 | 78 | class _LazyDescr(object): 79 | 80 | def __init__(self, name): 81 | self.name = name 82 | 83 | def __get__(self, obj, tp): 84 | result = self._resolve() 85 | setattr(obj, self.name, result) 86 | # This is a bit ugly, but it avoids running this again. 87 | delattr(tp, self.name) 88 | return result 89 | 90 | 91 | class MovedModule(_LazyDescr): 92 | 93 | def __init__(self, name, old, new=None): 94 | super(MovedModule, self).__init__(name) 95 | if PY3: 96 | if new is None: 97 | new = name 98 | self.mod = new 99 | else: 100 | self.mod = old 101 | 102 | def _resolve(self): 103 | return _import_module(self.mod) 104 | 105 | 106 | class MovedAttribute(_LazyDescr): 107 | 108 | def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None): 109 | super(MovedAttribute, self).__init__(name) 110 | if PY3: 111 | if new_mod is None: 112 | new_mod = name 113 | self.mod = new_mod 114 | if new_attr is None: 115 | if old_attr is None: 116 | new_attr = name 117 | else: 118 | new_attr = old_attr 119 | self.attr = new_attr 120 | else: 121 | self.mod = old_mod 122 | if old_attr is None: 123 | old_attr = name 124 | self.attr = old_attr 125 | 126 | def _resolve(self): 127 | module = _import_module(self.mod) 128 | return getattr(module, self.attr) 129 | 130 | 131 | 132 | class _MovedItems(types.ModuleType): 133 | """Lazy loading of moved objects""" 134 | 135 | 136 | _moved_attributes = [ 137 | MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"), 138 | MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"), 139 | MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"), 140 | MovedAttribute("map", "itertools", "builtins", "imap", "map"), 141 | MovedAttribute("reload_module", "__builtin__", "imp", "reload"), 142 | MovedAttribute("reduce", "__builtin__", "functools"), 143 | MovedAttribute("StringIO", "StringIO", "io"), 144 | MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"), 145 | MovedAttribute("zip", "itertools", "builtins", "izip", "zip"), 146 | 147 | MovedModule("builtins", "__builtin__"), 148 | MovedModule("configparser", "ConfigParser"), 149 | MovedModule("copyreg", "copy_reg"), 150 | MovedModule("http_cookiejar", "cookielib", "http.cookiejar"), 151 | MovedModule("http_cookies", "Cookie", "http.cookies"), 152 | MovedModule("html_entities", "htmlentitydefs", "html.entities"), 153 | MovedModule("html_parser", "HTMLParser", "html.parser"), 154 | MovedModule("http_client", "httplib", "http.client"), 155 | MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"), 156 | MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"), 157 | MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"), 158 | MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"), 159 | MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"), 160 | MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"), 161 | MovedModule("cPickle", "cPickle", "pickle"), 162 | MovedModule("queue", "Queue"), 163 | MovedModule("reprlib", "repr"), 164 | MovedModule("socketserver", "SocketServer"), 165 | MovedModule("tkinter", "Tkinter"), 166 | MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"), 167 | MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"), 168 | MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"), 169 | MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"), 170 | MovedModule("tkinter_tix", "Tix", "tkinter.tix"), 171 | MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"), 172 | MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"), 173 | MovedModule("tkinter_colorchooser", "tkColorChooser", 174 | "tkinter.colorchooser"), 175 | MovedModule("tkinter_commondialog", "tkCommonDialog", 176 | "tkinter.commondialog"), 177 | MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"), 178 | MovedModule("tkinter_font", "tkFont", "tkinter.font"), 179 | MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"), 180 | MovedModule("tkinter_tksimpledialog", "tkSimpleDialog", 181 | "tkinter.simpledialog"), 182 | MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"), 183 | MovedModule("winreg", "_winreg"), 184 | ] 185 | for attr in _moved_attributes: 186 | setattr(_MovedItems, attr.name, attr) 187 | del attr 188 | 189 | moves = sys.modules[__name__ + ".moves"] = _MovedItems("moves") 190 | 191 | 192 | def add_move(move): 193 | """Add an item to six.moves.""" 194 | setattr(_MovedItems, move.name, move) 195 | 196 | 197 | def remove_move(name): 198 | """Remove item from six.moves.""" 199 | try: 200 | delattr(_MovedItems, name) 201 | except AttributeError: 202 | try: 203 | del moves.__dict__[name] 204 | except KeyError: 205 | raise AttributeError("no such move, %r" % (name,)) 206 | 207 | 208 | if PY3: 209 | _meth_func = "__func__" 210 | _meth_self = "__self__" 211 | 212 | _func_closure = "__closure__" 213 | _func_code = "__code__" 214 | _func_defaults = "__defaults__" 215 | _func_globals = "__globals__" 216 | 217 | _iterkeys = "keys" 218 | _itervalues = "values" 219 | _iteritems = "items" 220 | _iterlists = "lists" 221 | else: 222 | _meth_func = "im_func" 223 | _meth_self = "im_self" 224 | 225 | _func_closure = "func_closure" 226 | _func_code = "func_code" 227 | _func_defaults = "func_defaults" 228 | _func_globals = "func_globals" 229 | 230 | _iterkeys = "iterkeys" 231 | _itervalues = "itervalues" 232 | _iteritems = "iteritems" 233 | _iterlists = "iterlists" 234 | 235 | 236 | try: 237 | advance_iterator = next 238 | except NameError: 239 | def advance_iterator(it): 240 | return it.next() 241 | next = advance_iterator 242 | 243 | 244 | try: 245 | callable = callable 246 | except NameError: 247 | def callable(obj): 248 | return any("__call__" in klass.__dict__ for klass in type(obj).__mro__) 249 | 250 | 251 | if PY3: 252 | def get_unbound_function(unbound): 253 | return unbound 254 | 255 | Iterator = object 256 | else: 257 | def get_unbound_function(unbound): 258 | return unbound.im_func 259 | 260 | class Iterator(object): 261 | 262 | def next(self): 263 | return type(self).__next__(self) 264 | 265 | callable = callable 266 | _add_doc(get_unbound_function, 267 | """Get the function out of a possibly unbound function""") 268 | 269 | 270 | get_method_function = operator.attrgetter(_meth_func) 271 | get_method_self = operator.attrgetter(_meth_self) 272 | get_function_closure = operator.attrgetter(_func_closure) 273 | get_function_code = operator.attrgetter(_func_code) 274 | get_function_defaults = operator.attrgetter(_func_defaults) 275 | get_function_globals = operator.attrgetter(_func_globals) 276 | 277 | 278 | def iterkeys(d, **kw): 279 | """Return an iterator over the keys of a dictionary.""" 280 | return iter(getattr(d, _iterkeys)(**kw)) 281 | 282 | def itervalues(d, **kw): 283 | """Return an iterator over the values of a dictionary.""" 284 | return iter(getattr(d, _itervalues)(**kw)) 285 | 286 | def iteritems(d, **kw): 287 | """Return an iterator over the (key, value) pairs of a dictionary.""" 288 | return iter(getattr(d, _iteritems)(**kw)) 289 | 290 | def iterlists(d, **kw): 291 | """Return an iterator over the (key, [values]) pairs of a dictionary.""" 292 | return iter(getattr(d, _iterlists)(**kw)) 293 | 294 | 295 | if PY3: 296 | def b(s): 297 | return s.encode("latin-1") 298 | def u(s): 299 | return s 300 | if sys.version_info[1] <= 1: 301 | def int2byte(i): 302 | return bytes((i,)) 303 | else: 304 | # This is about 2x faster than the implementation above on 3.2+ 305 | int2byte = operator.methodcaller("to_bytes", 1, "big") 306 | import io 307 | StringIO = io.StringIO 308 | BytesIO = io.BytesIO 309 | else: 310 | def b(s): 311 | return s 312 | def u(s): 313 | return unicode(s, "unicode_escape") 314 | int2byte = chr 315 | import StringIO 316 | StringIO = BytesIO = StringIO.StringIO 317 | _add_doc(b, """Byte literal""") 318 | _add_doc(u, """Text literal""") 319 | 320 | 321 | if PY3: 322 | import builtins 323 | exec_ = getattr(builtins, "exec") 324 | 325 | 326 | def reraise(tp, value, tb=None): 327 | if value.__traceback__ is not tb: 328 | raise value.with_traceback(tb) 329 | raise value 330 | 331 | 332 | print_ = getattr(builtins, "print") 333 | del builtins 334 | 335 | else: 336 | def exec_(_code_, _globs_=None, _locs_=None): 337 | """Execute code in a namespace.""" 338 | if _globs_ is None: 339 | frame = sys._getframe(1) 340 | _globs_ = frame.f_globals 341 | if _locs_ is None: 342 | _locs_ = frame.f_locals 343 | del frame 344 | elif _locs_ is None: 345 | _locs_ = _globs_ 346 | exec("""exec _code_ in _globs_, _locs_""") 347 | 348 | 349 | exec_("""def reraise(tp, value, tb=None): 350 | raise tp, value, tb 351 | """) 352 | 353 | 354 | def print_(*args, **kwargs): 355 | """The new-style print function.""" 356 | fp = kwargs.pop("file", sys.stdout) 357 | if fp is None: 358 | return 359 | def write(data): 360 | if not isinstance(data, basestring): 361 | data = str(data) 362 | fp.write(data) 363 | want_unicode = False 364 | sep = kwargs.pop("sep", None) 365 | if sep is not None: 366 | if isinstance(sep, unicode): 367 | want_unicode = True 368 | elif not isinstance(sep, str): 369 | raise TypeError("sep must be None or a string") 370 | end = kwargs.pop("end", None) 371 | if end is not None: 372 | if isinstance(end, unicode): 373 | want_unicode = True 374 | elif not isinstance(end, str): 375 | raise TypeError("end must be None or a string") 376 | if kwargs: 377 | raise TypeError("invalid keyword arguments to print()") 378 | if not want_unicode: 379 | for arg in args: 380 | if isinstance(arg, unicode): 381 | want_unicode = True 382 | break 383 | if want_unicode: 384 | newline = unicode("\n") 385 | space = unicode(" ") 386 | else: 387 | newline = "\n" 388 | space = " " 389 | if sep is None: 390 | sep = space 391 | if end is None: 392 | end = newline 393 | for i, arg in enumerate(args): 394 | if i: 395 | write(sep) 396 | write(arg) 397 | write(end) 398 | 399 | _add_doc(reraise, """Reraise an exception.""") 400 | 401 | 402 | def with_metaclass(meta, base=object): 403 | """Create a base class with a metaclass.""" 404 | return meta("NewBase", (base,), {}) 405 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Umeng Crash Symbolicator 5 | 6 | Umeng Crash Symbolicator ← 拖到书签栏 7 | -------------------------------------------------------------------------------- /symbol_server.py: -------------------------------------------------------------------------------- 1 | import sys, os, re, glob, subprocess, biplist 2 | import tornado.ioloop, tornado.web, tornado.template 3 | from tornado.web import asynchronous 4 | 5 | 6 | serverroot = os.path.abspath(os.path.dirname(__file__)) 7 | symbolsdir = os.path.join(serverroot, "symbols") 8 | templateLoader = tornado.template.Loader(serverroot) 9 | serverhost = 'localhost' 10 | serverport = '80' 11 | 12 | # dSYM UDID => (dir, app bundle, dSYM) 13 | symbolVersions = {} 14 | 15 | 16 | def loadVersions(): 17 | for dir in os.walk(symbolsdir).next()[1]: 18 | dir = os.path.join(symbolsdir, dir) 19 | os.chdir(dir) 20 | apps = glob.glob('*.app') 21 | dsyms = glob.glob('*.dSYM') 22 | if not apps or not dsyms: 23 | print "Error: invalid version", dir 24 | continue 25 | dsym = os.path.join(dir, dsyms[0]) 26 | udid = getDsymUDID(dsym) 27 | if not udid: 28 | print "Error: invalid dSYM", dsym 29 | continue 30 | symbolVersions[udid] = (dir, apps[0], dsyms[0]) 31 | #print symbolVersions 32 | 33 | def getDsymUDID(dsym): 34 | # mdls output format: 35 | # ( 36 | # "15BF90AE-951A-3DE7-B5E8-311E3D14C320" 37 | # ) 38 | udid = subprocess.check_output("mdls -name com_apple_xcode_dsym_uuids -raw \"" + dsym + "\"", shell=True) 39 | return re.findall(r'".+?"', udid)[0][1:-1] 40 | 41 | def getAppExecutable(app): 42 | plist = biplist.readPlist(os.path.join(app, "Info.plist")) 43 | return plist["CFBundleExecutable"] 44 | 45 | def getSymbol(dsymUDID, address, slide, baseAddr, arch): 46 | if dsymUDID not in symbolVersions: 47 | return None 48 | dir, app, _ = symbolVersions[dsymUDID] 49 | executable = getAppExecutable(app) 50 | realAddr = hex(int(address, 16) + int(slide, 16) - int(baseAddr, 16)) 51 | os.chdir(dir) 52 | cmd = "xcrun atos -arch %s -o '%s'/'%s' %s" % (arch, app.decode("utf-8"), executable, realAddr) 53 | output = subprocess.check_output(cmd, shell=True) 54 | output = re.sub(r"\(in .+?\)\s", "", output).strip() 55 | return output 56 | 57 | # crash log format: 58 | # *** -[NSDictionary initWithDictionary:copyItems:]: dictionary argument is not an NSDictionary 59 | # (null) 60 | # ( 61 | # 0 CoreFoundation 0x328632bb + 186 62 | # 1 libobjc.A.dylib 0x3a70e97f objc_exception_throw + 30 63 | # 2 CoreFoundation 0x327efff5 + 212 64 | # 3 CoreFoundation 0x327eff0b + 42 65 | # 4 ???????????? 0x0009ff8f ???????????? + 233359 66 | # 5 ???????????? 0x0009fefb ???????????? + 233211 67 | # 29 libsystem_c.dylib 0x3ab651d8 thread_start + 8 68 | # ) 69 | # 70 | # dSYM UUID: 15BF90AE-951A-3DE7-B5E8-311E3D14C320 71 | # CPU Type: armv7 72 | # Slide Address: 0x00001000 73 | # Binary Image: ???? 74 | # Base Address: 0x00067000 75 | # 76 | # - or like this - 77 | # 78 | # 5 AppBinaryImage 0x4d1841 _ZN6GBIter7AdvanceEjPb + 36 79 | # 6 AppBinaryImage 0x2bb1d1 _ZNSt6vectorIcSaIcEE13_M_assign + 1648 80 | 81 | def convertUmengCrashReport(report): 82 | dsymUDID = re.search(r"dSYM UUID:\s*(.+)\s*", report).group(1) 83 | arch = re.search(r"CPU Type:\s*(.+)\s*", report).group(1) 84 | slide = re.search(r"Slide Address:\s*(.+)\s*", report).group(1) 85 | baseAddr = re.search(r"Base Address:\s*(.+)\s*", report).group(1) 86 | imageName = re.search(r"Binary Image:\s*(.+)\s*", report).group(1) 87 | 88 | def replaceUnknownSymbol_Question(match): 89 | addr = match.group(2) 90 | return match.group(1) + getSymbol(dsymUDID, addr, slide, baseAddr, arch) 91 | 92 | def replaceUnknownSymbol_Normal(match): 93 | addr = match.group(2) 94 | return match.group(1) + getSymbol(dsymUDID, addr, '0', '0', arch) 95 | 96 | report = re.sub(r"((0x.+)\s+)(\?+|_mh_execute_header).*", replaceUnknownSymbol_Question, report) 97 | report = re.sub(r"(" + imageName + r"\s+(0x.+?)\s+).*", replaceUnknownSymbol_Normal, report) 98 | return report 99 | 100 | 101 | class MainHandler(tornado.web.RequestHandler): 102 | @asynchronous 103 | def get(self): 104 | self.finish(templateLoader.load("index.html").generate(host=serverhost, port=serverport)) 105 | 106 | class ConvertHandler(tornado.web.RequestHandler): 107 | def set_default_headers(self): 108 | self.set_header("Access-Control-Allow-Origin", "*") 109 | 110 | @asynchronous 111 | def post(self): 112 | report = self.get_argument("crashreport") 113 | self.finish(convertUmengCrashReport(report)) 114 | 115 | application = tornado.web.Application([ 116 | (r"/", MainHandler), 117 | (r"/convert", ConvertHandler), 118 | ], gzip=True) 119 | 120 | 121 | if __name__ == "__main__": 122 | if len(sys.argv) >= 2: 123 | serverhost = sys.argv[1] 124 | if len(sys.argv) >= 3: 125 | serverport = sys.argv[2] 126 | 127 | loadVersions() 128 | if not symbolVersions: 129 | print "No symbols found at", symbolsdir 130 | exit(1) 131 | 132 | application.listen(serverport) 133 | print "Server started -", "http://%s:%s"%(serverhost, serverport) 134 | tornado.ioloop.IOLoop.instance().start() 135 | --------------------------------------------------------------------------------