1#!/usr/bin/python
2# vim:sw=4:ts=4:et:
3# This Source Code Form is subject to the terms of the Mozilla Public
4# License, v. 2.0. If a copy of the MPL was not distributed with this
5# file, You can obtain one at http://mozilla.org/MPL/2.0/.
6
7# This script uses |atos| to post-process the entries produced by
8# NS_FormatCodeAddress(), which on Mac often lack a file name and a line
9# number.
10
11import subprocess
12import sys
13import re
14import os
15import pty
16import termios
17
18class unbufferedLineConverter:
19    """
20    Wrap a child process that responds to each line of input with one line of
21    output.  Uses pty to trick the child into providing unbuffered output.
22    """
23    def __init__(self, command, args = []):
24        pid, fd = pty.fork()
25        if pid == 0:
26            # We're the child.  Transfer control to command.
27            os.execvp(command, [command] + args)
28        else:
29            # Disable echoing.
30            attr = termios.tcgetattr(fd)
31            attr[3] = attr[3] & ~termios.ECHO
32            termios.tcsetattr(fd, termios.TCSANOW, attr)
33            # Set up a file()-like interface to the child process
34            self.r = os.fdopen(fd, "r", 1)
35            self.w = os.fdopen(os.dup(fd), "w", 1)
36    def convert(self, line):
37        self.w.write(line + "\n")
38        return self.r.readline().rstrip("\r\n")
39    @staticmethod
40    def test():
41        assert unbufferedLineConverter("rev").convert("123") == "321"
42        assert unbufferedLineConverter("cut", ["-c3"]).convert("abcde") == "c"
43        print "Pass"
44
45def separate_debug_file_for(file):
46    return None
47
48address_adjustments = {}
49def address_adjustment(file):
50    if not file in address_adjustments:
51        result = None
52        otool = subprocess.Popen(["otool", "-l", file], stdout=subprocess.PIPE)
53        while True:
54            line = otool.stdout.readline()
55            if line == "":
56                break
57            if line == "  segname __TEXT\n":
58                line = otool.stdout.readline()
59                if not line.startswith("   vmaddr "):
60                    raise StandardError("unexpected otool output")
61                result = int(line[10:], 16)
62                break
63        otool.stdout.close()
64
65        if result is None:
66            raise StandardError("unexpected otool output")
67
68        address_adjustments[file] = result
69
70    return address_adjustments[file]
71
72atoses = {}
73def addressToSymbol(file, address):
74    converter = None
75    if not file in atoses:
76        debug_file = separate_debug_file_for(file) or file
77        converter = unbufferedLineConverter('/usr/bin/xcrun', ['atos', '-arch', 'x86_64', '-o', debug_file])
78        atoses[file] = converter
79    else:
80        converter = atoses[file]
81    return converter.convert("0x%X" % address)
82
83cxxfilt_proc = None
84def cxxfilt(sym):
85    if cxxfilt_proc is None:
86        # --no-strip-underscores because atos already stripped the underscore
87        globals()["cxxfilt_proc"] = subprocess.Popen(['c++filt',
88                                                      '--no-strip-underscores',
89                                                      '--format', 'gnu-v3'],
90                                                     stdin=subprocess.PIPE,
91                                                     stdout=subprocess.PIPE)
92    cxxfilt_proc.stdin.write(sym + "\n")
93    return cxxfilt_proc.stdout.readline().rstrip("\n")
94
95# Matches lines produced by NS_FormatCodeAddress().
96line_re = re.compile("^(.*#\d+: )(.+)\[(.+) \+(0x[0-9A-Fa-f]+)\](.*)$")
97atos_name_re = re.compile("^(.+) \(in ([^)]+)\) \((.+)\)$")
98
99def fixSymbols(line):
100    result = line_re.match(line)
101    if result is not None:
102        (before, fn, file, address, after) = result.groups()
103        address = int(address, 16)
104
105        if os.path.exists(file) and os.path.isfile(file):
106            address += address_adjustment(file)
107            info = addressToSymbol(file, address)
108
109            # atos output seems to have three forms:
110            #   address
111            #   address (in foo.dylib)
112            #   symbol (in foo.dylib) (file:line)
113            name_result = atos_name_re.match(info)
114            if name_result is not None:
115                # Print the first two forms as-is, and transform the third
116                (name, library, fileline) = name_result.groups()
117                # atos demangles, but occasionally it fails.  cxxfilt can mop
118                # up the remaining cases(!), which will begin with '_Z'.
119                if (name.startswith("_Z")):
120                    name = cxxfilt(name)
121                info = "%s (%s, in %s)" % (name, fileline, library)
122
123            nl = '\n' if line[-1] == '\n' else ''
124            return before + info + after + nl
125        else:
126            sys.stderr.write("Warning: File \"" + file + "\" does not exist.\n")
127            return line
128    else:
129        return line
130
131if __name__ == "__main__":
132    for line in sys.stdin:
133        sys.stdout.write(fixSymbols(line))
134