Kevin Lubick | a9246dd | 2017-02-15 10:20:30 -0500 | [diff] [blame] | 1 | #!/usr/bin/env python |
| 2 | # Copyright 2017 The Chromium Authors. All rights reserved. |
| 3 | # Use of this source code is governed by a BSD-style license that can be |
| 4 | # found in the LICENSE file. |
| 5 | # pylint: disable=line-too-long |
| 6 | |
Kevin Lubick | 0548a21 | 2017-02-20 08:48:35 -0500 | [diff] [blame] | 7 | import collections |
Kevin Lubick | a9246dd | 2017-02-15 10:20:30 -0500 | [diff] [blame] | 8 | import os |
| 9 | import re |
| 10 | import subprocess |
| 11 | import sys |
| 12 | |
| 13 | # Run a command and symbolize anything that looks like a stacktrace in the |
| 14 | # stdout/stderr. This will return with the same error code as the command. |
| 15 | |
| 16 | # First parameter is the current working directory, which will be stripped |
| 17 | # out of stacktraces. The rest of the parameters will be fed to |
| 18 | # subprocess.check_output() and should be the command and arguments that |
| 19 | # will be fed in. If any environment variables are set when running this |
| 20 | # script, they will be automatically used by the call to |
| 21 | # subprocess.check_output(). |
| 22 | |
| 23 | # This wrapper function is needed to make sure stdout and stderr stay properly |
| 24 | # interleaved, to assist in debugging. There are no clean ways to achieve |
| 25 | # this with recipes. For example, running the dm step with parameters like |
| 26 | # stdout=api.raw_io.output(), stderr=api.raw_io.output() ended up with |
| 27 | # stderr and stdout being separate files, which eliminated the interwoven logs. |
| 28 | # Aside from specifying stdout/stderr, there are no ways to capture or reason |
| 29 | # about the logs of previous steps without using a wrapper like this. |
| 30 | |
| 31 | def main(basedir, cmd): |
Kevin Lubick | 0548a21 | 2017-02-20 08:48:35 -0500 | [diff] [blame] | 32 | logs = collections.deque(maxlen=200) |
Kevin Lubick | a9246dd | 2017-02-15 10:20:30 -0500 | [diff] [blame] | 33 | |
Kevin Lubick | 0548a21 | 2017-02-20 08:48:35 -0500 | [diff] [blame] | 34 | proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, |
| 35 | stderr=subprocess.STDOUT) |
| 36 | for line in iter(proc.stdout.readline, ''): |
| 37 | sys.stdout.write(line) |
| 38 | logs.append(line) |
| 39 | proc.wait() |
| 40 | print 'Command exited with code %s' % proc.returncode |
Kevin Lubick | a9246dd | 2017-02-15 10:20:30 -0500 | [diff] [blame] | 41 | # Stacktraces generally look like: |
| 42 | # /lib/x86_64-linux-gnu/libc.so.6(abort+0x16a) [0x7fa90e8d0c62] |
| 43 | # /b/s/w/irISUIyA/linux_vulkan_intel_driver_debug/./libvulkan_intel.so(+0x1f4d0a) [0x7fa909eead0a] |
| 44 | # /b/s/w/irISUIyA/out/Debug/dm() [0x17c3c5f] |
| 45 | # The stack_line regex splits those into three parts. Experimentation has |
| 46 | # shown that the address in () works best for external libraries, but our code |
| 47 | # doesn't have that. So, we capture both addresses and prefer using the first |
| 48 | # over the second, unless the first is blank or invalid. Relative offsets |
| 49 | # like abort+0x16a are ignored. |
| 50 | stack_line = r'^(?P<path>.+)\(\+?(?P<addr>.*)\) \[(?P<addr2>.+)\]' |
| 51 | # After performing addr2line, the result can be something obnoxious like: |
| 52 | # foo(bar) at /b/s/w/a39kd/Skia/out/Clang/../../src/gpu/Frobulator.cpp:13 |
| 53 | # The extra_path strips off the not-useful prefix and leaves just the |
| 54 | # important src/gpu/Frobulator.cpp:13 bit. |
| 55 | extra_path = r'/.*\.\./' |
Kevin Lubick | 0548a21 | 2017-02-20 08:48:35 -0500 | [diff] [blame] | 56 | is_first = True |
| 57 | for line in logs: |
Kevin Lubick | a9246dd | 2017-02-15 10:20:30 -0500 | [diff] [blame] | 58 | line = line.strip() |
| 59 | |
| 60 | m = re.search(stack_line, line) |
| 61 | if m: |
Kevin Lubick | 0548a21 | 2017-02-20 08:48:35 -0500 | [diff] [blame] | 62 | if is_first: |
| 63 | print '#######################################' |
| 64 | print 'symbolized stacktrace follows' |
| 65 | print '#######################################' |
| 66 | is_first = False |
| 67 | |
Kevin Lubick | a9246dd | 2017-02-15 10:20:30 -0500 | [diff] [blame] | 68 | path = m.group('path') |
| 69 | addr = m.group('addr') |
| 70 | addr2 = m.group('addr2') |
| 71 | if os.path.exists(path): |
| 72 | if not addr or not addr.startswith('0x'): |
| 73 | addr = addr2 |
| 74 | sym = subprocess.check_output(['addr2line', '-Cfpe', path, addr]) |
| 75 | sym = sym.strip() |
| 76 | # If addr2line doesn't return anything useful, we don't replace the |
| 77 | # original address, so the human can see it. |
| 78 | if sym and not sym.startswith('?'): |
| 79 | if path.startswith(basedir): |
| 80 | path = path[len(basedir)+1:] |
| 81 | sym = re.sub(extra_path, '', sym) |
| 82 | line = path + ' ' + sym |
Kevin Lubick | 0548a21 | 2017-02-20 08:48:35 -0500 | [diff] [blame] | 83 | print line |
Kevin Lubick | a9246dd | 2017-02-15 10:20:30 -0500 | [diff] [blame] | 84 | |
Kevin Lubick | 0548a21 | 2017-02-20 08:48:35 -0500 | [diff] [blame] | 85 | sys.exit(proc.returncode) |
Kevin Lubick | a9246dd | 2017-02-15 10:20:30 -0500 | [diff] [blame] | 86 | |
| 87 | |
| 88 | if __name__ == '__main__': |
| 89 | if len(sys.argv) < 3: |
| 90 | print >> sys.stderr, 'USAGE: %s working_dir cmd_and_args...' % sys.argv[0] |
| 91 | sys.exit(1) |
| 92 | main(sys.argv[1], sys.argv[2:]) |