import argparse import cStringIO import errno import gzip import json import logging import os import re import requests import shutil import subprocess import sys import tempfile import traceback LDD_RE = re.compile(r'^\t([^ ]+) => ([^ ]+) ', re.MULTILINE) OTOOL_RE = re.compile(r'^\t([^ ]+) \(', re.MULTILINE) DEFAULT_CRASHREPORTING_HOSTNAME = "crashes.clementine-player.org" DEFAULT_SYMBOLS_DIRECTORY = "symbols" DEFAULT_DUMP_SYMS_BINARY = "3rdparty/google-breakpad/dump_syms" SYMBOLUPLOAD_URL = "http://%s/api/upload/symbols" class BaseDumperImpl(object): def GetLinkedLibraries(self, filename): raise NotImplementedError def DebugSymbolsFilename(self, filename): class Context(object): def __enter__(self): return filename def __exit__(self, exc_type, exc_value, traceback): pass return Context() class LinuxDumperImpl(BaseDumperImpl): def GetLinkedLibraries(self, filename): stdout = subprocess.check_output(["ldd", filename]) for match in LDD_RE.finditer(stdout): yield os.path.realpath(match.group(2)) class MacDumperImpl(BaseDumperImpl): def GetLinkedLibraries(self, filename): stdout = subprocess.check_output(["otool", "-L", filename]) executable_path = os.path.dirname(filename) for match in OTOOL_RE.finditer(stdout): path = match.group(1) path = path.replace("@executable_path", executable_path) path = path.replace("@loader_path", executable_path) yield path def DebugSymbolsFilename(self, filename): class Context(object): def __init__(self): self.temp_dir = tempfile.mkdtemp() self.temp_file = os.path.join(self.temp_dir, os.path.basename(filename)) def __enter__(self): logging.info("Reading dwarf symbols from '%s'", filename) # Extract the dwarf symbols into the temporary file dsymutil = subprocess.Popen( ["dsymutil", filename, "-f", "-o", self.temp_file], stdout=subprocess.PIPE) stdout, _ = dsymutil.communicate() if "no debug symbols" in stdout: return filename else: return self.temp_file def __exit__(self, exc_type, exc_value, traceback): shutil.rmtree(self.temp_dir) return Context() class Dumper(object): def __init__(self, symbols_directory, dump_syms_binary): self.symbols_directory = symbols_directory self.dump_syms_binary = dump_syms_binary self.visited_executables = set() self.binary_namehashes = [] if sys.platform == "darwin": self.impl = MacDumperImpl() else: self.impl = LinuxDumperImpl() def NormaliseFilename(self, filename): return os.path.abspath(filename) def SymbolFilename(self, binary_namehash): return os.path.join( self.symbols_directory, binary_namehash[0], binary_namehash[1], binary_namehash[0] + ".sym") def Dump(self, binary_filename): # Check we haven't processed this file before. binary_filename = self.NormaliseFilename(binary_filename) if binary_filename in self.visited_executables: return self.visited_executables.add(binary_filename) if not os.path.exists(binary_filename): logging.warning("Skipping nonexistent file '%s'", binary_filename) return # Run dump_syms with self.impl.DebugSymbolsFilename(binary_filename) as symbol_filename: symbol_directory = os.path.dirname(symbol_filename) handle = subprocess.Popen( [self.dump_syms_binary, symbol_filename, symbol_directory], stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = handle.communicate() if handle.returncode != 0: if ("Failed to open debug ELF file" in stderr or "does not contain a .gnu_debuglink section" in stderr): logging.warning("Skipping file with missing debug symbols: '%s'", binary_filename) return raise Exception("dump_syms failed for %s\nstdout: %s\nstderr: %s" % ( binary_filename, stdout, stderr)) # The first line of the output contains the hash. first_line = stdout[0:stdout.find("\n")] _, os_name, architecture, binary_hash, binary_name = first_line.split(" ") # Decide where to write the symbol file. binary_namehash = (binary_name, binary_hash) symbol_filename = self.SymbolFilename(binary_namehash) self.binary_namehashes.append(binary_namehash) logging.info("Writing '%s'", symbol_filename) # Create parent directories. try: os.makedirs(os.path.dirname(symbol_filename)) except OSError, ex: # It's fine if the directory already exists. if ex.errno != errno.EEXIST: raise # Output the file. with open(symbol_filename, "w") as handle: handle.write(stdout) # Now do the same for any linked libraries. for path in self.impl.GetLinkedLibraries(binary_filename): self.Dump(path) class Uploader(object): def __init__(self, crashreporting_hostname, dumper): self.crashreporting_hostname = crashreporting_hostname self.dumper = dumper def Compress(self, filename): stringio = cStringIO.StringIO() with gzip.GzipFile(fileobj=stringio, mode="w") as gzip_file: with open(filename) as original_file: shutil.copyfileobj(original_file, gzip_file) original_size = os.path.getsize(filename) new_size = stringio.tell() logging.info("Compressed %s from %d bytes to %d bytes (%02f%%)" % ( filename, original_size, new_size, float(new_size) / original_size * 100)) stringio.seek(0) return stringio def UploadMissing(self): name_hashes = {"%s/%s" % x: x for x in self.dumper.binary_namehashes} # Which symbols aren't on the server yet? url = SYMBOLUPLOAD_URL % self.crashreporting_hostname response = requests.post(url, data={ "symbol": name_hashes.keys(), }).json() for path, url in response["urls"].items(): try: symbol_filename = self.dumper.SymbolFilename(name_hashes[path]) logging.info("Uploading '%s'", symbol_filename) upload_response = requests.put(url, data=self.Compress(symbol_filename).read(), headers={'content-type': 'application/gzip'}) upload_response.raise_for_status() except Exception: logging.warning("Failed to upload file '%s': %s" % ( symbol_filename, traceback.format_exc())) def main(): parser = argparse.ArgumentParser() parser.add_argument("--symbols_directory", default=DEFAULT_SYMBOLS_DIRECTORY) parser.add_argument("--dump_syms_binary", default=DEFAULT_DUMP_SYMS_BINARY) parser.add_argument("--crashreporting_hostname", default=DEFAULT_CRASHREPORTING_HOSTNAME) parser.add_argument("filenames", nargs='+') args = parser.parse_args() logging.basicConfig(level=logging.DEBUG, format="%(asctime)s %(levelname)-7s %(message)s") logging.getLogger("requests").setLevel(logging.WARNING) dumper = Dumper(args.symbols_directory, args.dump_syms_binary) uploader = Uploader(args.crashreporting_hostname, dumper) for filename in args.filenames: dumper.Dump(filename) uploader.UploadMissing() if __name__ == "__main__": main()