Skip to content

Instantly share code, notes, and snippets.

@tiennou
Created April 17, 2017 17:00
Show Gist options
  • Save tiennou/6519afd04954abd2f12dfdffc932f3ef to your computer and use it in GitHub Desktop.
Save tiennou/6519afd04954abd2f12dfdffc932f3ef to your computer and use it in GitHub Desktop.
QS Crash report wrangler
# A Ruby module for OS X crash-report-wrangling
#
# https://developer.apple.com/library/content/technotes/tn2004/tn2123.html
require 'digest/md5'
require 'shellwords'
$debug = false
def debug(str)
puts str if $debug
end
module CrashReport
class ParseException < Exception; end
class InternalInconsitency < Exception; end # because I like consistency
def self.parse(file)
CrashLog.new(file)
end
class CrashLog
class Header
attr_reader :raw_name
attr_reader :raw_value
attr_reader :identifier
attr_reader :value
def initialize(name, value)
@raw_name = name
@raw_value = value
@identifier = begin
h = @raw_name.dup
h.downcase!
h.gsub!(/[^a-zA-Z]/, "_")
h.to_sym
end
convert_value
end
def convert_value
@value ||= case
when (i = Integer(@raw_value) rescue nil) then
i
when identifier == :crashed_thread then
@raw_value.to_i
else
@raw_value
end
@value
end
def to_int; value; end
def to_i; value; end
def to_str; value; end
def to_s; value; end
def to_sym; identifier; end
end
class Thread
class Frame
attr_reader :num, :bundle, :addr, :symbol, :offset
def initialize(num, bundle, addr, pos)
@num = num
@bundle = bundle
@addr = addr
@symbol, @offset = pos.split ' + '
end
def to_s
format
end
def format(indent = 0, w = nil)
w ||= Hash.new { 0 }
fmt = "%s%#{w[:num]}d - %-#{w[:bundle]}s % #{w[:addr]}s %- #{w[:symbol]}s + %d\n"
fmt % [" " * indent, num, bundle, addr, symbol, offset]
end
def width_stats
Hash[[:num, :bundle, :addr].map {|k| [k, self.send(k).length] } ]
end
end
attr_reader :num, :name, :frames
def self.parse(num, name, fd)
self.new(num, name) do |t|
while ((stack = fd.gets.strip) != "") do
frame = stack.split(/\s+/, 4)
t.frames << Frame.new(*frame)
end
end
end
def initialize(num, name)
@num = num
@name = name
@frames = []
yield self
end
def parse_regfile(fd)
while (line = fd.gets) do
if line =~ /^Binary Images:$/
CrashLog.rewind_line(fd, line)
break
end
# WIP
end
end
end
class Image
attr_reader :offset_start, :offset_end, :plus, :bundle, :version, :uuid, :path
def initialize(offset_start, offset_end, plus, bundle, version, uuid, path)
@offset_start = offset_start
@offset_end = offset_end
@bundle = bundle
@plus = (plus == "+")
@version = version
@uuid = uuid
@path = path
end
end
attr_reader :file # crashlog file
attr_reader :headers # {} by symbolized name
attr_reader :threads # [] by thread num
attr_reader :images # [] by address
def self.rewind_line(fd, line)
fd.seek(-line.length, IO::SEEK_CUR)
end
def extract_headers(fd)
debug("extracting headers")
while (line = fd.gets) do
if line =~ /Thread \d+::/
CrashLog.rewind_line(fd, line)
break
end
if (line =~ /^([^:]+):\s+(.*)$/)
h = Header.new($1, $2)
@headers[h.to_sym] = h
@version = $2.to_i if h.to_sym == :report_version
end
end
end
def extract_threads(fd)
debug("extracting threads")
while (line = fd.gets) do
if line =~ /Binary Images:/
CrashLog.rewind_line(fd, line)
break
end
if (line =~ /^Thread (\d+)(?: Crashed)?:(?:: (.*)|)$/)
@threads << Thread.parse($1, $2, fd)
elsif (line =~ /^Thread (\d+) crashed with/)
tid = $1.to_i
@threads[tid].parse_regfile(fd)
end
end
end
def extract_images(fd)
debug("extracting images")
while (line = fd.gets) do
next if line =~/^Binary Images:$/
if (line =~ /^\s+(0x[0-9a-f]+)\s-\s+(0x[0-9a-f]+)\s+(\+?)([^\s]+)\s\(([^)]+)\)\s+<([-0-9A-F]+)>\s(.*)$/)
begin
@images << Image.new(*$~.captures)
rescue Exception
raise ParseException.new("unable to parse image from line #{fd.lineno}")
end
end
end
end
def initialize(file)
@file = file
@headers = {}
@threads = []
@images = []
$debug = file =~ /NormansiMac/
::File.open(file, 'r') do |f|
extract_headers(f)
extract_threads(f)
extract_images(f)
# extract_ext_mods(f)
# extract_vm_regions(f)
end
end
def file_name
File.basename(@file)
end
def bundle_identifier
headers[:identifier].to_s
end
def bundle_version
headers[:version].to_s
end
def crashed_thread_id
headers[:crashed_thread].to_i
end
def crashed_thread
@threads[crashed_thread_id]
end
def summary
str = <<-EOR.gsub(/ {8}/, '')
Crash report for "#{bundle_identifier}", version #{bundle_version}
#{images.count} images loaded, #{threads.count} threads, thread #{crashed_thread_id} crashed.
EOR
str += "Thread #{crashed_thread.num}: \"#{crashed_thread.name}\"\n"
w = {}
crashed_thread.frames.each do |f|
s = f.width_stats
w.update(s) {|k, a, b| a > b ? a : b }
end
crashed_thread.frames.each do |f|
str += f.format(2, w)
end
str
end
end
class Librarian
class Report
attr_reader :crashlog
def initialize(crashlog)
@crashlog = crashlog
@t = crashlog.crashed_thread
end
def our_bundles
@bundles ||= @crashlog.images.select {|i| i.plus }.map {|i| i.bundle }
end
def crashed_frame; @t.frames.first; end
def crashed_symbol; crashed_frame.symbol; end
def own_frames
@t.frames.select {|f| our_bundles.include?(f.bundle) }
end
def to_s
"Crash in #{crashed_frame.symbol}: #{own_frames.reverse.map {|f| f.symbol}.join ' > '}"
end
end
attr_reader :reports
attr_reader :bundle_identifier
attr_reader :bundle_version
def initialize
@bundle_identifier = nil
@bundle_version = nil
@reports = []
@crashes_in = {}
end
def import(crashlog)
@bundle_identifier ||= crashlog.bundle_identifier
@bundle_version ||= crashlog.bundle_version
if bundle_identifier != crashlog.bundle_identifier
raise InternalInconsitency.new("report for a different bundle ID: #{bundle_identifier} != #{crashlog.bundle_identifier}")
end
if bundle_version != crashlog.bundle_version
raise InternalInconsitency.new("report for a different bundle version: #{bundle_version} != #{crashlog.bundle_version}")
end
r = Report.new(crashlog)
@crashes_in[r.crashed_symbol] ||= []
@crashes_in[r.crashed_symbol] << r
@reports << r
end
def summary
puts "#{@reports.count} crashlogs seen."
@crashes_in.sort_by {|sym, reports| reports.count }.each do |symbol, reports|
puts "Crashes in \"#{symbol}\": #{reports.count} reports"
files = {}
counts = {}
stacks = {}
reports.each do |r|
stack = r.own_frames.map {|f| f.symbol }.join " > "
stack = "(none)" if stack.empty?
h = Digest::MD5.hexdigest(stack)
stacks[h] = stack
files[h] ||= []
files[h] << r.crashlog.file_name.shellescape
counts[h] ||= 0
counts[h] += 1
end
puts
if stacks.count > 5
puts " Stacks:"
stacks.sort_by {|h, s| counts[h] }.reverse.each do |hash, stack|
puts " - #{hash} => #{stack}"
puts " Files: #{files[hash].join " "}"
end
puts
puts " Tally:"
counts.sort_by {|h, c| c }.reverse.each do |hash, count|
puts " #{hash} - #{count}" + (stacks[hash] == "(none)" ? " (none)" : "")
end
puts
else
puts " Tally:"
counts.sort_by {|h, c| c }.reverse.each do |hash, count|
stack = stacks[hash]
puts " #{count} - #{stack}"
end
puts
end
end
end
end
end
#!/usr/bin/env ruby -W
require 'pp'
$:.unshift File.dirname(__FILE__)
require 'crashreport'
lib = CrashReport::Librarian.new
ARGV.each do |file|
puts "Parsing #{file}"
crashlog = CrashReport.parse(file)
begin
lib.import(crashlog)
rescue Exception => e
puts "Invalid report #{file}: #{e}"
end
end
lib.summary
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment