Created
April 17, 2017 17:00
-
-
Save tiennou/6519afd04954abd2f12dfdffc932f3ef to your computer and use it in GitHub Desktop.
QS Crash report wrangler
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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