michael@0: #!/usr/bin/python michael@0: # vim:sw=4:ts=4:et: michael@0: # This Source Code Form is subject to the terms of the Mozilla Public michael@0: # License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: # file, You can obtain one at http://mozilla.org/MPL/2.0/. michael@0: michael@0: # This script uses atos to process the output of nsTraceRefcnt's Mac OS michael@0: # X stack walking code. This is useful for two things: michael@0: # (1) Getting line number information out of michael@0: # |nsTraceRefcnt::WalkTheStack|'s output in debug builds. michael@0: # (2) Getting function names out of |nsTraceRefcnt::WalkTheStack|'s michael@0: # output on all builds (where it mostly prints UNKNOWN because only michael@0: # a handful of symbols are exported from component libraries). michael@0: # michael@0: # Use the script by piping output containing stacks (such as raw stacks michael@0: # or make-tree.pl balance trees) through this script. michael@0: michael@0: import subprocess michael@0: import sys michael@0: import re michael@0: import os michael@0: import pty michael@0: import termios michael@0: michael@0: class unbufferedLineConverter: michael@0: """ michael@0: Wrap a child process that responds to each line of input with one line of michael@0: output. Uses pty to trick the child into providing unbuffered output. michael@0: """ michael@0: def __init__(self, command, args = []): michael@0: pid, fd = pty.fork() michael@0: if pid == 0: michael@0: # We're the child. Transfer control to command. michael@0: os.execvp(command, [command] + args) michael@0: else: michael@0: # Disable echoing. michael@0: attr = termios.tcgetattr(fd) michael@0: attr[3] = attr[3] & ~termios.ECHO michael@0: termios.tcsetattr(fd, termios.TCSANOW, attr) michael@0: # Set up a file()-like interface to the child process michael@0: self.r = os.fdopen(fd, "r", 1) michael@0: self.w = os.fdopen(os.dup(fd), "w", 1) michael@0: def convert(self, line): michael@0: self.w.write(line + "\n") michael@0: return self.r.readline().rstrip("\r\n") michael@0: @staticmethod michael@0: def test(): michael@0: assert unbufferedLineConverter("rev").convert("123") == "321" michael@0: assert unbufferedLineConverter("cut", ["-c3"]).convert("abcde") == "c" michael@0: print "Pass" michael@0: michael@0: def separate_debug_file_for(file): michael@0: return None michael@0: michael@0: address_adjustments = {} michael@0: def address_adjustment(file): michael@0: if not file in address_adjustments: michael@0: result = None michael@0: otool = subprocess.Popen(["otool", "-l", file], stdout=subprocess.PIPE) michael@0: while True: michael@0: line = otool.stdout.readline() michael@0: if line == "": michael@0: break michael@0: if line == " segname __TEXT\n": michael@0: line = otool.stdout.readline() michael@0: if not line.startswith(" vmaddr "): michael@0: raise StandardError("unexpected otool output") michael@0: result = int(line[10:], 16) michael@0: break michael@0: otool.stdout.close() michael@0: michael@0: if result is None: michael@0: raise StandardError("unexpected otool output") michael@0: michael@0: address_adjustments[file] = result michael@0: michael@0: return address_adjustments[file] michael@0: michael@0: atoses = {} michael@0: def addressToSymbol(file, address): michael@0: converter = None michael@0: if not file in atoses: michael@0: debug_file = separate_debug_file_for(file) or file michael@0: converter = unbufferedLineConverter('/usr/bin/xcrun', ['atos', '-arch', 'x86_64', '-o', debug_file]) michael@0: atoses[file] = converter michael@0: else: michael@0: converter = atoses[file] michael@0: return converter.convert("0x%X" % address) michael@0: michael@0: cxxfilt_proc = None michael@0: def cxxfilt(sym): michael@0: if cxxfilt_proc is None: michael@0: # --no-strip-underscores because atos already stripped the underscore michael@0: globals()["cxxfilt_proc"] = subprocess.Popen(['c++filt', michael@0: '--no-strip-underscores', michael@0: '--format', 'gnu-v3'], michael@0: stdin=subprocess.PIPE, michael@0: stdout=subprocess.PIPE) michael@0: cxxfilt_proc.stdin.write(sym + "\n") michael@0: return cxxfilt_proc.stdout.readline().rstrip("\n") michael@0: michael@0: line_re = re.compile("^(.*) ?\[([^ ]*) \+(0x[0-9a-fA-F]{1,8})\](.*)$") michael@0: balance_tree_re = re.compile("^([ \|0-9-]*)") michael@0: atos_name_re = re.compile("^(.+) \(in ([^)]+)\) \((.+)\)$") michael@0: michael@0: def fixSymbols(line): michael@0: result = line_re.match(line) michael@0: if result is not None: michael@0: # before allows preservation of balance trees michael@0: # after allows preservation of counts michael@0: (before, file, address, after) = result.groups() michael@0: address = int(address, 16) michael@0: michael@0: if os.path.exists(file) and os.path.isfile(file): michael@0: address += address_adjustment(file) michael@0: info = addressToSymbol(file, address) michael@0: michael@0: # atos output seems to have three forms: michael@0: # address michael@0: # address (in foo.dylib) michael@0: # symbol (in foo.dylib) (file:line) michael@0: name_result = atos_name_re.match(info) michael@0: if name_result is not None: michael@0: # Print the first two forms as-is, and transform the third michael@0: (name, library, fileline) = name_result.groups() michael@0: # atos demangles, but occasionally it fails. cxxfilt can mop michael@0: # up the remaining cases(!), which will begin with '_Z'. michael@0: if (name.startswith("_Z")): michael@0: name = cxxfilt(name) michael@0: info = "%s (%s, in %s)" % (name, fileline, library) michael@0: michael@0: # throw away the bad symbol, but keep balance tree structure michael@0: before = balance_tree_re.match(before).groups()[0] michael@0: michael@0: return before + info + after + "\n" michael@0: else: michael@0: sys.stderr.write("Warning: File \"" + file + "\" does not exist.\n") michael@0: return line michael@0: else: michael@0: return line michael@0: michael@0: if __name__ == "__main__": michael@0: for line in sys.stdin: michael@0: sys.stdout.write(fixSymbols(line))