1#!/usr/bin/python
2
3#----------------------------------------------------------------------
4# Be sure to add the python path that points to the LLDB shared library.
5#
6# To use this in the embedded python interpreter using "lldb":
7#
8#   cd /path/containing/crashlog.py
9#   lldb
10#   (lldb) script import crashlog
11#   "crashlog" command installed, type "crashlog --help" for detailed help
12#   (lldb) crashlog ~/Library/Logs/DiagnosticReports/a.crash
13#
14# The benefit of running the crashlog command inside lldb in the
15# embedded python interpreter is when the command completes, there
16# will be a target with all of the files loaded at the locations
17# described in the crash log. Only the files that have stack frames
18# in the backtrace will be loaded unless the "--load-all" option
19# has been specified. This allows users to explore the program in the
20# state it was in right at crash time.
21#
22# On MacOSX csh, tcsh:
23#   ( setenv PYTHONPATH /path/to/LLDB.framework/Resources/Python ; ./crashlog.py ~/Library/Logs/DiagnosticReports/a.crash )
24#
25# On MacOSX sh, bash:
26#   PYTHONPATH=/path/to/LLDB.framework/Resources/Python ./crashlog.py ~/Library/Logs/DiagnosticReports/a.crash
27#----------------------------------------------------------------------
28
29import lldb
30import commands
31import cmd
32import glob
33import optparse
34import os
35import plistlib
36import pprint # pp = pprint.PrettyPrinter(indent=4); pp.pprint(command_args)
37import re
38import shlex
39import string
40import sys
41import time
42import uuid
43from lldb.utils import symbolication
44
45PARSE_MODE_NORMAL = 0
46PARSE_MODE_THREAD = 1
47PARSE_MODE_IMAGES = 2
48PARSE_MODE_THREGS = 3
49PARSE_MODE_SYSTEM = 4
50
51class CrashLog(symbolication.Symbolicator):
52    """Class that does parses darwin crash logs"""
53    thread_state_regex = re.compile('^Thread ([0-9]+) crashed with')
54    thread_regex = re.compile('^Thread ([0-9]+)([^:]*):(.*)')
55    frame_regex = re.compile('^([0-9]+) +([^ ]+) *\t(0x[0-9a-fA-F]+) +(.*)')
56    image_regex_uuid = re.compile('(0x[0-9a-fA-F]+)[- ]+(0x[0-9a-fA-F]+) +[+]?([^ ]+) +([^<]+)<([-0-9a-fA-F]+)> (.*)');
57    image_regex_no_uuid = re.compile('(0x[0-9a-fA-F]+)[- ]+(0x[0-9a-fA-F]+) +[+]?([^ ]+) +([^/]+)/(.*)');
58    empty_line_regex = re.compile('^$')
59
60    class Thread:
61        """Class that represents a thread in a darwin crash log"""
62        def __init__(self, index):
63            self.index = index
64            self.frames = list()
65            self.registers = dict()
66            self.reason = None
67            self.queue = None
68
69        def dump(self, prefix):
70            print "%sThread[%u] %s" % (prefix, self.index, self.reason)
71            if self.frames:
72                print "%s  Frames:" % (prefix)
73                for frame in self.frames:
74                    frame.dump(prefix + '    ')
75            if self.registers:
76                print "%s  Registers:" % (prefix)
77                for reg in self.registers.keys():
78                    print "%s    %-5s = %#16.16x" % (prefix, reg, self.registers[reg])
79
80        def did_crash(self):
81            return self.reason != None
82
83        def __str__(self):
84            s = "Thread[%u]" % self.index
85            if self.reason:
86                s += ' %s' % self.reason
87            return s
88
89
90    class Frame:
91        """Class that represents a stack frame in a thread in a darwin crash log"""
92        def __init__(self, index, pc, description):
93            self.pc = pc
94            self.description = description
95            self.index = index
96
97        def __str__(self):
98            if self.description:
99                return "[%3u] 0x%16.16x %s" % (self.index, self.pc, self.description)
100            else:
101                return "[%3u] 0x%16.16x" % (self.index, self.pc)
102
103        def dump(self, prefix):
104            print "%s%s" % (prefix, str(self))
105
106    class DarwinImage(symbolication.Image):
107        """Class that represents a binary images in a darwin crash log"""
108        dsymForUUIDBinary = os.path.expanduser('~rc/bin/dsymForUUID')
109        if not os.path.exists(dsymForUUIDBinary):
110            dsymForUUIDBinary = commands.getoutput('which dsymForUUID')
111
112        dwarfdump_uuid_regex = re.compile('UUID: ([-0-9a-fA-F]+) \(([^\(]+)\) .*')
113
114        def __init__(self, text_addr_lo, text_addr_hi, identifier, version, uuid, path):
115            symbolication.Image.__init__(self, path, uuid);
116            self.add_section (symbolication.Section(text_addr_lo, text_addr_hi, "__TEXT"))
117            self.identifier = identifier
118            self.version = version
119
120        def locate_module_and_debug_symbols(self):
121            # Don't load a module twice...
122            if self.resolved:
123                return True
124            # Mark this as resolved so we don't keep trying
125            self.resolved = True
126            uuid_str = self.get_normalized_uuid_string()
127            print 'Getting symbols for %s %s...' % (uuid_str, self.path),
128            if os.path.exists(self.dsymForUUIDBinary):
129                dsym_for_uuid_command = '%s %s' % (self.dsymForUUIDBinary, uuid_str)
130                s = commands.getoutput(dsym_for_uuid_command)
131                if s:
132                    plist_root = plistlib.readPlistFromString (s)
133                    if plist_root:
134                        plist = plist_root[uuid_str]
135                        if plist:
136                            if 'DBGArchitecture' in plist:
137                                self.arch = plist['DBGArchitecture']
138                            if 'DBGDSYMPath' in plist:
139                                self.symfile = os.path.realpath(plist['DBGDSYMPath'])
140                            if 'DBGSymbolRichExecutable' in plist:
141                                self.resolved_path = os.path.expanduser (plist['DBGSymbolRichExecutable'])
142            if not self.resolved_path and os.path.exists(self.path):
143                dwarfdump_cmd_output = commands.getoutput('dwarfdump --uuid "%s"' % self.path)
144                self_uuid = self.get_uuid()
145                for line in dwarfdump_cmd_output.splitlines():
146                    match = self.dwarfdump_uuid_regex.search (line)
147                    if match:
148                        dwarf_uuid_str = match.group(1)
149                        dwarf_uuid = uuid.UUID(dwarf_uuid_str)
150                        if self_uuid == dwarf_uuid:
151                            self.resolved_path = self.path
152                            self.arch = match.group(2)
153                            break;
154                if not self.resolved_path:
155                    self.unavailable = True
156                    print "error\n    error: unable to locate '%s' with UUID %s" % (self.path, uuid_str)
157                    return False
158            if (self.resolved_path and os.path.exists(self.resolved_path)) or (self.path and os.path.exists(self.path)):
159                print 'ok'
160                # if self.resolved_path:
161                #     print '  exe = "%s"' % self.resolved_path
162                # if self.symfile:
163                #     print ' dsym = "%s"' % self.symfile
164                return True
165            else:
166                self.unavailable = True
167            return False
168
169
170
171    def __init__(self, path):
172        """CrashLog constructor that take a path to a darwin crash log file"""
173        symbolication.Symbolicator.__init__(self);
174        self.path = os.path.expanduser(path);
175        self.info_lines = list()
176        self.system_profile = list()
177        self.threads = list()
178        self.idents = list() # A list of the required identifiers for doing all stack backtraces
179        self.crashed_thread_idx = -1
180        self.version = -1
181        self.error = None
182        # With possible initial component of ~ or ~user replaced by that user's home directory.
183        try:
184            f = open(self.path)
185        except IOError:
186            self.error = 'error: cannot open "%s"' % self.path
187            return
188
189        self.file_lines = f.read().splitlines()
190        parse_mode = PARSE_MODE_NORMAL
191        thread = None
192        for line in self.file_lines:
193            # print line
194            line_len = len(line)
195            if line_len == 0:
196                if thread:
197                    if parse_mode == PARSE_MODE_THREAD:
198                        if thread.index == self.crashed_thread_idx:
199                            thread.reason = ''
200                            if self.thread_exception:
201                                thread.reason += self.thread_exception
202                            if self.thread_exception_data:
203                                thread.reason += " (%s)" % self.thread_exception_data
204                        self.threads.append(thread)
205                    thread = None
206                else:
207                    # only append an extra empty line if the previous line
208                    # in the info_lines wasn't empty
209                    if len(self.info_lines) > 0 and len(self.info_lines[-1]):
210                        self.info_lines.append(line)
211                parse_mode = PARSE_MODE_NORMAL
212                # print 'PARSE_MODE_NORMAL'
213            elif parse_mode == PARSE_MODE_NORMAL:
214                if line.startswith ('Process:'):
215                    (self.process_name, pid_with_brackets) = line[8:].strip().split()
216                    self.process_id = pid_with_brackets.strip('[]')
217                elif line.startswith ('Path:'):
218                    self.process_path = line[5:].strip()
219                elif line.startswith ('Identifier:'):
220                    self.process_identifier = line[11:].strip()
221                elif line.startswith ('Version:'):
222                    version_string = line[8:].strip()
223                    matched_pair = re.search("(.+)\((.+)\)", version_string)
224                    if matched_pair:
225                        self.process_version = matched_pair.group(1)
226                        self.process_compatability_version = matched_pair.group(2)
227                    else:
228                        self.process = version_string
229                        self.process_compatability_version = version_string
230                elif line.startswith ('Parent Process:'):
231                    (self.parent_process_name, pid_with_brackets) = line[15:].strip().split()
232                    self.parent_process_id = pid_with_brackets.strip('[]')
233                elif line.startswith ('Exception Type:'):
234                    self.thread_exception = line[15:].strip()
235                    continue
236                elif line.startswith ('Exception Codes:'):
237                    self.thread_exception_data = line[16:].strip()
238                    continue
239                elif line.startswith ('Crashed Thread:'):
240                    self.crashed_thread_idx = int(line[15:].strip().split()[0])
241                    continue
242                elif line.startswith ('Report Version:'):
243                    self.version = int(line[15:].strip())
244                    continue
245                elif line.startswith ('System Profile:'):
246                    parse_mode = PARSE_MODE_SYSTEM
247                    continue
248                elif (line.startswith ('Interval Since Last Report:') or
249                      line.startswith ('Crashes Since Last Report:') or
250                      line.startswith ('Per-App Interval Since Last Report:') or
251                      line.startswith ('Per-App Crashes Since Last Report:') or
252                      line.startswith ('Sleep/Wake UUID:') or
253                      line.startswith ('Anonymous UUID:')):
254                    # ignore these
255                    continue
256                elif line.startswith ('Thread'):
257                    thread_state_match = self.thread_state_regex.search (line)
258                    if thread_state_match:
259                        thread_state_match = self.thread_regex.search (line)
260                        thread_idx = int(thread_state_match.group(1))
261                        parse_mode = PARSE_MODE_THREGS
262                        thread = self.threads[thread_idx]
263                    else:
264                        thread_match = self.thread_regex.search (line)
265                        if thread_match:
266                            # print 'PARSE_MODE_THREAD'
267                            parse_mode = PARSE_MODE_THREAD
268                            thread_idx = int(thread_match.group(1))
269                            thread = CrashLog.Thread(thread_idx)
270                    continue
271                elif line.startswith ('Binary Images:'):
272                    parse_mode = PARSE_MODE_IMAGES
273                    continue
274                self.info_lines.append(line.strip())
275            elif parse_mode == PARSE_MODE_THREAD:
276                if line.startswith ('Thread'):
277                    continue
278                frame_match = self.frame_regex.search(line)
279                if frame_match:
280                    ident = frame_match.group(2)
281                    if not ident in self.idents:
282                        self.idents.append(ident)
283                    thread.frames.append (CrashLog.Frame(int(frame_match.group(1)), int(frame_match.group(3), 0), frame_match.group(4)))
284                else:
285                    print 'error: frame regex failed for line: "%s"' % line
286            elif parse_mode == PARSE_MODE_IMAGES:
287                image_match = self.image_regex_uuid.search (line)
288                if image_match:
289                    image = CrashLog.DarwinImage (int(image_match.group(1),0),
290                                                  int(image_match.group(2),0),
291                                                  image_match.group(3).strip(),
292                                                  image_match.group(4).strip(),
293                                                  uuid.UUID(image_match.group(5)),
294                                                  image_match.group(6))
295                    self.images.append (image)
296                else:
297                    image_match = self.image_regex_no_uuid.search (line)
298                    if image_match:
299                        image = CrashLog.DarwinImage (int(image_match.group(1),0),
300                                                      int(image_match.group(2),0),
301                                                      image_match.group(3).strip(),
302                                                      image_match.group(4).strip(),
303                                                      None,
304                                                      image_match.group(5))
305                        self.images.append (image)
306                    else:
307                        print "error: image regex failed for: %s" % line
308
309            elif parse_mode == PARSE_MODE_THREGS:
310                stripped_line = line.strip()
311                reg_values = re.split('  +', stripped_line);
312                for reg_value in reg_values:
313                    #print 'reg_value = "%s"' % reg_value
314                    (reg, value) = reg_value.split(': ')
315                    #print 'reg = "%s"' % reg
316                    #print 'value = "%s"' % value
317                    thread.registers[reg.strip()] = int(value, 0)
318            elif parse_mode == PARSE_MODE_SYSTEM:
319                self.system_profile.append(line)
320        f.close()
321
322    def dump(self):
323        print "Crash Log File: %s" % (self.path)
324        print "\nThreads:"
325        for thread in self.threads:
326            thread.dump('  ')
327        print "\nImages:"
328        for image in self.images:
329            image.dump('  ')
330
331    def find_image_with_identifier(self, identifier):
332        for image in self.images:
333            if image.identifier == identifier:
334                return image
335        return None
336
337    def create_target(self):
338        #print 'crashlog.create_target()...'
339        target = symbolication.Symbolicator.create_target(self)
340        if target:
341            return target
342        # We weren't able to open the main executable as, but we can still symbolicate
343        print 'crashlog.create_target()...2'
344        if self.idents:
345            for ident in self.idents:
346                image = self.find_image_with_identifier (ident)
347                if image:
348                    target = image.create_target ()
349                    if target:
350                        return target # success
351        print 'crashlog.create_target()...3'
352        for image in self.images:
353            target = image.create_target ()
354            if target:
355                return target # success
356        print 'crashlog.create_target()...4'
357        print 'error: unable to locate any executables from the crash log'
358        return None
359
360
361def usage():
362    print "Usage: lldb-symbolicate.py [-n name] executable-image"
363    sys.exit(0)
364
365class Interactive(cmd.Cmd):
366    '''Interactive prompt for analyzing one or more Darwin crash logs, type "help" to see a list of supported commands.'''
367    image_option_parser = None
368
369    def __init__(self, crash_logs):
370        cmd.Cmd.__init__(self)
371        self.intro = 'Interactive crashlogs prompt, type "help" to see a list of supported commands.'
372        self.crash_logs = crash_logs
373        self.prompt = '% '
374
375    def default(self, line):
376        '''Catch all for unknown command, which will exit the interpreter.'''
377        print "uknown command: %s" % line
378        return True
379
380    def do_q(self, line):
381        '''Quit command'''
382        return True
383
384    def do_quit(self, line):
385        '''Quit command'''
386        return True
387
388    def do_symbolicate(self, line):
389        description='''Symbolicate one or more darwin crash log files by index to provide source file and line information,
390        inlined stack frames back to the concrete functions, and disassemble the location of the crash
391        for the first frame of the crashed thread.'''
392        option_parser = CreateSymbolicateCrashLogOptions ('symbolicate', description, False)
393        command_args = shlex.split(line)
394        try:
395            (options, args) = option_parser.parse_args(command_args)
396        except:
397            return
398
399        for idx_str in args:
400            idx = int(idx_str)
401            if idx < len(self.crash_logs):
402                SymbolicateCrashLog (self.crash_logs[idx], options)
403            else:
404                print 'error: crash log index %u is out of range' % (idx)
405
406    def do_list(self, line=None):
407        '''Dump a list of all crash logs that are currently loaded.
408
409        USAGE: list'''
410        print '%u crash logs are loaded:' % len(self.crash_logs)
411        for (crash_log_idx, crash_log) in enumerate(self.crash_logs):
412            print '[%u] = %s' % (crash_log_idx, crash_log.path)
413
414    def do_image(self, line):
415        '''Dump information about an image in the crash log given an image basename.
416
417        USAGE: image <basename>'''
418        usage = "usage: %prog [options] <PATH> [PATH ...]"
419        description='''Dump information about one or more images in all crash logs. The <PATH>
420        can be a full path or a image basename.'''
421        command_args = shlex.split(line)
422        if not self.image_option_parser:
423            self.image_option_parser = optparse.OptionParser(description=description, prog='image',usage=usage)
424            self.image_option_parser.add_option('-a', '--all', action='store_true', help='show all images', default=False)
425        try:
426            (options, args) = self.image_option_parser.parse_args(command_args)
427        except:
428            return
429
430        if args:
431            for image_path in args:
432                fullpath_search = image_path[0] == '/'
433                for crash_log in self.crash_logs:
434                    matches_found = 0
435                    for (image_idx, image) in enumerate(crash_log.images):
436                        if fullpath_search:
437                            if image.get_resolved_path() == image_path:
438                                matches_found += 1
439                                print image
440                        else:
441                            image_basename = image.get_resolved_path_basename()
442                            if image_basename == image_path:
443                                matches_found += 1
444                                print image
445                    if matches_found == 0:
446                        for (image_idx, image) in enumerate(crash_log.images):
447                            resolved_image_path = image.get_resolved_path()
448                            if resolved_image_path and string.find(image.get_resolved_path(), image_path) >= 0:
449                                print image
450        else:
451            for crash_log in self.crash_logs:
452                for (image_idx, image) in enumerate(crash_log.images):
453                    print '[%u] %s' % (image_idx, image)
454        return False
455
456
457def interactive_crashlogs(options, args):
458    crash_log_files = list()
459    for arg in args:
460        for resolved_path in glob.glob(arg):
461            crash_log_files.append(resolved_path)
462
463    crash_logs = list();
464    for crash_log_file in crash_log_files:
465        #print 'crash_log_file = "%s"' % crash_log_file
466        crash_log = CrashLog(crash_log_file)
467        if crash_log.error:
468            print crash_log.error
469            continue
470        if options.verbose:
471            crash_log.dump()
472        if not crash_log.images:
473            print 'error: no images in crash log "%s"' % (crash_log)
474            continue
475        else:
476            crash_logs.append(crash_log)
477
478    interpreter = Interactive(crash_logs)
479    # List all crash logs that were imported
480    interpreter.do_list()
481    interpreter.cmdloop()
482
483
484def Symbolicate(debugger, command, result, dict):
485    try:
486        SymbolicateCrashLogs (shlex.split(command))
487    except:
488        result.PutCString ("error: python exception %s" % sys.exc_info()[0])
489
490def SymbolicateCrashLog(crash_log, options):
491    if crash_log.error:
492        print crash_log.error
493        return
494    if options.verbose:
495        crash_log.dump()
496    if not crash_log.images:
497        print 'error: no images in crash log'
498        return
499
500    target = crash_log.create_target ()
501    if not target:
502        return
503    exe_module = target.GetModuleAtIndex(0)
504    images_to_load = list()
505    loaded_images = list()
506    if options.load_all_images:
507        # --load-all option was specified, load everything up
508        for image in crash_log.images:
509            images_to_load.append(image)
510    else:
511        # Only load the images found in stack frames for the crashed threads
512        for ident in crash_log.idents:
513            images = crash_log.find_images_with_identifier (ident)
514            if images:
515                for image in images:
516                    images_to_load.append(image)
517            else:
518                print 'error: can\'t find image for identifier "%s"' % ident
519
520    for image in images_to_load:
521        if image in loaded_images:
522            print "warning: skipping %s loaded at %#16.16x duplicate entry (probably commpage)" % (image.path, image.text_addr_lo)
523        else:
524            err = image.add_module (target)
525            if err:
526                print err
527            else:
528                #print 'loaded %s' % image
529                loaded_images.append(image)
530
531    for thread in crash_log.threads:
532        this_thread_crashed = thread.did_crash()
533        if options.crashed_only and this_thread_crashed == False:
534            continue
535        print "%s" % thread
536        #prev_frame_index = -1
537        for frame_idx, frame in enumerate(thread.frames):
538            disassemble = (this_thread_crashed or options.disassemble_all_threads) and frame_idx < options.disassemble_depth;
539            if frame_idx == 0:
540                symbolicated_frame_addresses = crash_log.symbolicate (frame.pc)
541            else:
542                # Any frame above frame zero and we have to subtract one to get the previous line entry
543                symbolicated_frame_addresses = crash_log.symbolicate (frame.pc - 1)
544
545            if symbolicated_frame_addresses:
546                symbolicated_frame_address_idx = 0
547                for symbolicated_frame_address in symbolicated_frame_addresses:
548                    print '[%3u] %s' % (frame_idx, symbolicated_frame_address)
549
550                    if symbolicated_frame_address_idx == 0:
551                        if disassemble:
552                            instructions = symbolicated_frame_address.get_instructions()
553                            if instructions:
554                                print
555                                symbolication.disassemble_instructions (target,
556                                                                        instructions,
557                                                                        frame.pc,
558                                                                        options.disassemble_before,
559                                                                        options.disassemble_after, frame.index > 0)
560                                print
561                    symbolicated_frame_address_idx += 1
562            else:
563                print frame
564        print
565
566    if options.dump_image_list:
567        print "Binary Images:"
568        for image in crash_log.images:
569            print image
570
571def CreateSymbolicateCrashLogOptions(command_name, description, add_interactive_options):
572    usage = "usage: %prog [options] <FILE> [FILE ...]"
573    option_parser = optparse.OptionParser(description=description, prog='crashlog',usage=usage)
574    option_parser.add_option('-v', '--verbose', action='store_true', dest='verbose', help='display verbose debug info', default=False)
575    option_parser.add_option('-a', '--load-all', action='store_true', dest='load_all_images', help='load all executable images, not just the images found in the crashed stack frames', default=False)
576    option_parser.add_option('--images', action='store_true', dest='dump_image_list', help='show image list', default=False)
577    option_parser.add_option('-g', '--debug-delay', type='int', dest='debug_delay', metavar='NSEC', help='pause for NSEC seconds for debugger', default=0)
578    option_parser.add_option('-c', '--crashed-only', action='store_true', dest='crashed_only', help='only symbolicate the crashed thread', default=False)
579    option_parser.add_option('-d', '--disasm-depth', type='int', dest='disassemble_depth', help='set the depth in stack frames that should be disassembled (default is 1)', default=1)
580    option_parser.add_option('-D', '--disasm-all', action='store_true', dest='disassemble_all_threads', help='enabled disassembly of frames on all threads (not just the crashed thread)', default=False)
581    option_parser.add_option('-B', '--disasm-before', type='int', dest='disassemble_before', help='the number of instructions to disassemble before the frame PC', default=4)
582    option_parser.add_option('-A', '--disasm-after', type='int', dest='disassemble_after', help='the number of instructions to disassemble after the frame PC', default=4)
583    if add_interactive_options:
584        option_parser.add_option('-i', '--interactive', action='store_true', help='parse all crash logs and enter interactive mode', default=False)
585    return option_parser
586
587def SymbolicateCrashLogs(command_args):
588    description='''Symbolicate one or more darwin crash log files to provide source file and line information,
589inlined stack frames back to the concrete functions, and disassemble the location of the crash
590for the first frame of the crashed thread.
591If this script is imported into the LLDB command interpreter, a "crashlog" command will be added to the interpreter
592for use at the LLDB command line. After a crash log has been parsed and symbolicated, a target will have been
593created that has all of the shared libraries loaded at the load addresses found in the crash log file. This allows
594you to explore the program as if it were stopped at the locations described in the crash log and functions can
595be disassembled and lookups can be performed using the addresses found in the crash log.'''
596    option_parser = CreateSymbolicateCrashLogOptions ('crashlog', description, True)
597    try:
598        (options, args) = option_parser.parse_args(command_args)
599    except:
600        return
601
602    if options.verbose:
603        print 'command_args = %s' % command_args
604        print 'options', options
605        print 'args', args
606
607    if options.debug_delay > 0:
608        print "Waiting %u seconds for debugger to attach..." % options.debug_delay
609        time.sleep(options.debug_delay)
610    error = lldb.SBError()
611
612    if args:
613        if options.interactive:
614            interactive_crashlogs(options, args)
615        else:
616            for crash_log_file in args:
617                crash_log = CrashLog(crash_log_file)
618                SymbolicateCrashLog (crash_log, options)
619if __name__ == '__main__':
620    # Create a new debugger instance
621    print 'main'
622    lldb.debugger = lldb.SBDebugger.Create()
623    SymbolicateCrashLogs (sys.argv[1:])
624elif getattr(lldb, 'debugger', None):
625    lldb.debugger.HandleCommand('command script add -f lldb.macosx.crashlog.Symbolicate crashlog')
626    print '"crashlog" command installed, type "crashlog --help" for detailed help'
627
628