tracetool 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/tracetool +2 -0
- data/lib/tracetool/android/java.rb +46 -0
- data/lib/tracetool/android/native.rb +156 -0
- data/lib/tracetool/android.rb +30 -0
- data/lib/tracetool/ios/parser.rb +18 -0
- data/lib/tracetool/ios/scanner.rb +78 -0
- data/lib/tracetool/ios.rb +11 -0
- data/lib/tracetool/utils/cli.rb +118 -0
- data/lib/tracetool/utils/env.rb +34 -0
- data/lib/tracetool/utils/parser.rb +104 -0
- data/lib/tracetool/utils/pipe.rb +32 -0
- data/lib/tracetool.rb +29 -0
- data/lib/version.rb +13 -0
- metadata +71 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 24b271eb9816bfe2c8c9cf44af633851dfd06497
|
4
|
+
data.tar.gz: f1c65ec08ac3aeedc355ec8160d8507cf1e1b73e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a2008ddb93477871d23e215cd4b7771e15b51942782f3e0a05b292d73e2c3e8b169f95fb4d2b3ce4a1c3c8bf7f007aeda8d68cf529a171999cb21ad9fdb187a2
|
7
|
+
data.tar.gz: 4509d9dd674ef506f88930e178a851da1af1167fe2985d490ecf0a5c3c083bc64d1c3b3f9699753b9492054875dda78a073b1600adc1040790aac82ff353b0ed
|
data/bin/tracetool
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require_relative '../utils/parser'
|
2
|
+
|
3
|
+
module Tracetool
|
4
|
+
module Android
|
5
|
+
# Parses java stack traces
|
6
|
+
class JavaTraceParser < Tracetool::BaseTraceParser
|
7
|
+
# Describes java stack entry
|
8
|
+
STACK_ENTRY_PATTERN = /^(\s+at (?<call_description>.+))|((?<error>.+?): (?<message>.+))$/
|
9
|
+
# Describes java method call
|
10
|
+
CALL_PATTERN = /(?<class>.+)\.(?<method>[^\(]+)\((((?<file>.+\.java):(?<line>\d+))|(?<location>.+))\)$/
|
11
|
+
|
12
|
+
def initialize(files)
|
13
|
+
super(STACK_ENTRY_PATTERN, CALL_PATTERN, files, true)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
# Processes java traces
|
17
|
+
class JavaTraceScanner
|
18
|
+
RX_FIRST_EXCEPTION_LINE = /^.+$/
|
19
|
+
RX_OTHER_EXCEPTION_LINE = /at [^(]+\(([^:]+:\d+)|(Native Method)\)$/
|
20
|
+
|
21
|
+
def initialize(string)
|
22
|
+
@trace = string
|
23
|
+
end
|
24
|
+
|
25
|
+
def process(_ctx)
|
26
|
+
@trace
|
27
|
+
end
|
28
|
+
|
29
|
+
class << self
|
30
|
+
def match(string)
|
31
|
+
# Split into lines
|
32
|
+
first, *rest = string.split("\n")
|
33
|
+
|
34
|
+
return if rest.nil? || rest.empty?
|
35
|
+
return unless RX_FIRST_EXCEPTION_LINE.match(first)
|
36
|
+
|
37
|
+
rest.all? { |line| RX_OTHER_EXCEPTION_LINE.match(line) }
|
38
|
+
end
|
39
|
+
|
40
|
+
def [](string)
|
41
|
+
new(string) if match(string)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
require_relative '../utils/parser'
|
2
|
+
|
3
|
+
module Tracetool
|
4
|
+
module Android
|
5
|
+
# Android traces scanner and mapper
|
6
|
+
class NativeTraceParser < Tracetool::BaseTraceParser
|
7
|
+
# Describes android stack entry
|
8
|
+
STACK_ENTRY_PATTERN =
|
9
|
+
%r{Stack frame #(?<frame>\d+) (?<address>\w+ [a-f\d]+) (?<lib>[/\w\d\.-]+)( )?(: (?<call_description>.+))?$}
|
10
|
+
# Describes android native method call (class::method and source file with line number)
|
11
|
+
CALL_PATTERN = /(Routine )?(?<method>.+) ((in)|(at)) (?<file>.+):(?<line>\d+)/
|
12
|
+
|
13
|
+
def initialize(files)
|
14
|
+
super(STACK_ENTRY_PATTERN, CALL_PATTERN, files, true)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Methods for stack trace string normalization
|
19
|
+
module NativeTraceEnhancer
|
20
|
+
# Default header for android backtrace
|
21
|
+
NATIVE_DUMP_HEADER = <<-BACKTRACE.strip_indent
|
22
|
+
*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
|
23
|
+
Build fingerprint: UNKNOWN
|
24
|
+
pid: 0, tid: 0
|
25
|
+
signal 0 (UNKNOWN)
|
26
|
+
backtrace:
|
27
|
+
BACKTRACE
|
28
|
+
|
29
|
+
# Converts packed stack trace into ndk-stack compatible format
|
30
|
+
# @param [String] trace packed stack trace
|
31
|
+
# @return well formed stack trace
|
32
|
+
def unpack(trace)
|
33
|
+
dump_body = prepare(trace).map.with_index { |line, index| convert_line(line, index) }
|
34
|
+
add_header(dump_body.join("\n"))
|
35
|
+
end
|
36
|
+
|
37
|
+
# Add dummy header for stack trace body
|
38
|
+
def add_header(string)
|
39
|
+
NATIVE_DUMP_HEADER + sanitize(string)
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def prepare(trace)
|
45
|
+
trace.gsub('>>><<<', '')[/<<<(.+)>>>/, 1].split(';')
|
46
|
+
end
|
47
|
+
|
48
|
+
def convert_line(line, index)
|
49
|
+
frame = index
|
50
|
+
addr = line[/^(-?\d+) (.*)$/, 1]
|
51
|
+
lib = line[/^(-?\d+) (.*)$/, 2].strip
|
52
|
+
' #%02i pc %08x %s'.format(frame, addr, lib)
|
53
|
+
end
|
54
|
+
|
55
|
+
# If needed here we'll drop all unneeded leading characters from each
|
56
|
+
# stack trace line. This may be needed to add NATIVE_DUMP_HEADER
|
57
|
+
# correctly
|
58
|
+
def sanitize(string)
|
59
|
+
string
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Processes native traces
|
64
|
+
class NativeTraceScanner
|
65
|
+
# Initial sequence of asterisks which marks begining of trace body
|
66
|
+
TRACE_DELIMETER = '*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***'.freeze
|
67
|
+
RX_INITIAL_ASTERISKS = /#{TRACE_DELIMETER.gsub('*', '\*')}/
|
68
|
+
# Contains address line like
|
69
|
+
#
|
70
|
+
# ```
|
71
|
+
# pc 00000000004321ec libfoo.so
|
72
|
+
# ```
|
73
|
+
RX_PC_ADDRESS = /pc \d+/
|
74
|
+
|
75
|
+
# Format of packed trace.
|
76
|
+
# Consists of one or more trace blocks.
|
77
|
+
# * Each block starts with `<<<` and ends with `>>>`.
|
78
|
+
# * Each block contains one or more lines
|
79
|
+
# * Lines delimited with ;
|
80
|
+
# * Line consists of
|
81
|
+
# ** pointer address `/\d+/`
|
82
|
+
# ** library (so) name `/[^ ]+/`
|
83
|
+
# ** symbol name `/[^ ]+/`, if present
|
84
|
+
# ** symbol offset `/\d+/`
|
85
|
+
#
|
86
|
+
# Last two entries can be missing.
|
87
|
+
RX_PACKED_FORMAT = /^(<<<(\d+ [^ ]+ ([^ ]+ \d+)?;)+>>>)+$/
|
88
|
+
|
89
|
+
# @param [String] string well formed native android stack trace
|
90
|
+
# @see https://developer.android.com/ndk/guides/ndk-stack.html
|
91
|
+
def initialize(string)
|
92
|
+
@trace = string
|
93
|
+
end
|
94
|
+
|
95
|
+
# @param [OpenStruct] ctx context object containing `symbols` field with
|
96
|
+
# path to symbols dir
|
97
|
+
# @return [String] desymbolicated stack trace
|
98
|
+
def process(ctx)
|
99
|
+
symbols = File.join(ctx.symbols, 'local')
|
100
|
+
symbols = if ctx.arch
|
101
|
+
File.join(symbols, ctx.arch)
|
102
|
+
else
|
103
|
+
Dir[File.join(symbols, '*')].first || symbols
|
104
|
+
end
|
105
|
+
Pipe['ndk-stack', '-sym', symbols] << @trace
|
106
|
+
end
|
107
|
+
|
108
|
+
class << self
|
109
|
+
# Add methods for trace normalization
|
110
|
+
include Tracetool::Android::NativeTraceEnhancer
|
111
|
+
# Tells if provided string is a ndk trace
|
112
|
+
# @return truthy or falsey value
|
113
|
+
def match(string)
|
114
|
+
return false if string.empty?
|
115
|
+
packed?(string) || with_header?(string) || without_header?(string)
|
116
|
+
end
|
117
|
+
|
118
|
+
def packed?(string)
|
119
|
+
RX_PACKED_FORMAT.match(string)
|
120
|
+
end
|
121
|
+
|
122
|
+
def without_header?(string)
|
123
|
+
lines = string.split("\n")
|
124
|
+
return true if address_lines?(lines)
|
125
|
+
|
126
|
+
first, *rest = lines
|
127
|
+
first.include?('backtrace:') && address_lines?(rest)
|
128
|
+
end
|
129
|
+
|
130
|
+
def address_lines?(lines)
|
131
|
+
lines.all? do |line|
|
132
|
+
RX_PC_ADDRESS.match(line)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def with_header?(string)
|
137
|
+
RX_INITIAL_ASTERISKS.match(string)
|
138
|
+
end
|
139
|
+
|
140
|
+
# With given potential stack trace string
|
141
|
+
# create scanner if possible
|
142
|
+
# @param [String] string trace
|
143
|
+
# @return [NativeTraceScanner] or nil
|
144
|
+
def [](string)
|
145
|
+
if packed? string
|
146
|
+
new(unpack(string))
|
147
|
+
elsif with_header? string
|
148
|
+
new(string)
|
149
|
+
elsif without_header? string
|
150
|
+
new(add_header(string))
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require_relative 'android/java'
|
2
|
+
require_relative 'android/native'
|
3
|
+
|
4
|
+
# Tracetool root module
|
5
|
+
module Tracetool
|
6
|
+
# Android trace scan variant
|
7
|
+
module Android
|
8
|
+
# Desymbolicates android traces
|
9
|
+
class AndroidTraceScanner
|
10
|
+
# List of scanners
|
11
|
+
SCANNERS = [JavaTraceScanner, NativeTraceScanner].freeze
|
12
|
+
|
13
|
+
# Launches process of trace desymbolication
|
14
|
+
# @param [String] trace trace body
|
15
|
+
def process(trace, context)
|
16
|
+
# Find scanner which maches trace format
|
17
|
+
scanner = SCANNERS.map { |s| s[trace] }.compact.first
|
18
|
+
raise(ArgumentError, "#{trace}\n not android trace?") unless scanner
|
19
|
+
scanner.process(context)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class << self
|
24
|
+
# Desymbolicate android stack trace
|
25
|
+
def scan(string, opts = {})
|
26
|
+
AndroidTraceScanner.new .process(string, OpenStruct.new(opts))
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require_relative '../utils/parser'
|
2
|
+
|
3
|
+
module Tracetool
|
4
|
+
module IOS
|
5
|
+
# IOS traces scanner and source mapper
|
6
|
+
class IOSTraceParser < Tracetool::BaseTraceParser
|
7
|
+
# Describes IOS stack entry
|
8
|
+
STACK_ENTRY_PATTERN = /^(?<frame>\d+) (?<binary>[^ ]+) (?<call_description>.+)$/
|
9
|
+
# Describes source block
|
10
|
+
SOURCE_PATTERN =
|
11
|
+
/^((-?\[(?<class>[^ ]+) (?<method>.+)\])|(?<method>.+)) \(in (?<module>.+)\) \((?<file>.+):(?<line>\d+)\)$/
|
12
|
+
|
13
|
+
def initialize(files)
|
14
|
+
super(STACK_ENTRY_PATTERN, SOURCE_PATTERN, files, true)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Tracetool
|
2
|
+
module IOS
|
3
|
+
# Converts context to atos arguments
|
4
|
+
class AtosContext
|
5
|
+
# If no arch specified will use `arm64`
|
6
|
+
DEFAULT_ARCH = 'arm64'.freeze
|
7
|
+
|
8
|
+
# List of required argument names
|
9
|
+
REQUIRED_ARGUMENTS = %i[load_address xarchive module_name].freeze
|
10
|
+
|
11
|
+
#
|
12
|
+
def initialize(ctx)
|
13
|
+
check_arguments(ctx)
|
14
|
+
@load_address = ctx.load_address
|
15
|
+
@binary_path = module_binary(ctx.xarchive, ctx.module_name)
|
16
|
+
@arch = ctx.arch || 'arm64'
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_args
|
20
|
+
%w[-o -l -arch].zip([@binary_path, @load_address, @arch]).flatten
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def module_binary(xarchive, module_name)
|
26
|
+
File.join(xarchive, 'dSYMs', "#{module_name}.app.dSYM", 'Contents', 'Resources', 'DWARF', module_name)
|
27
|
+
end
|
28
|
+
|
29
|
+
def check_arguments(ctx)
|
30
|
+
REQUIRED_ARGUMENTS.each do |a|
|
31
|
+
ctx[a] || raise(ArgumentError, "Missing `#{a}` value")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# launches atos
|
37
|
+
class IOSTraceScanner
|
38
|
+
# Stack trace line consists of numerous whitespace separated
|
39
|
+
# columns. First three always are:
|
40
|
+
#
|
41
|
+
# * frame #
|
42
|
+
# * binary name
|
43
|
+
# * address
|
44
|
+
# @param [String] trace string containing stack trace
|
45
|
+
# @return [Array] containing (%binary_name%, %address%) pairs
|
46
|
+
def parse(trace)
|
47
|
+
trace.split("\n").map do |line|
|
48
|
+
line.split(' ')[1..2] # Fetch binary name and address
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def process(trace, context)
|
53
|
+
trace = parse(trace)
|
54
|
+
desym = run_atos(context, trace.map(&:last))
|
55
|
+
# Add useful columns to unpacked trace
|
56
|
+
mix(trace, desym.split("\n")).join("\n")
|
57
|
+
end
|
58
|
+
|
59
|
+
def run_atos(context, trace)
|
60
|
+
Pipe['atos', *AtosContext.new(context).to_args] << trace
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def mix(trace, symbolicated)
|
66
|
+
trace.zip(symbolicated).map.with_index do |pair, i|
|
67
|
+
t, desym = pair
|
68
|
+
line = []
|
69
|
+
line << i
|
70
|
+
line << t.first
|
71
|
+
line << desym
|
72
|
+
|
73
|
+
line.join(' ')
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'optparse/time'
|
3
|
+
require 'ostruct'
|
4
|
+
|
5
|
+
module Tracetool
|
6
|
+
# Tracetool cli args parser
|
7
|
+
class ParseArgs
|
8
|
+
# List of supported abis
|
9
|
+
ARCH_LIST = %i[armeabi-v7a armeabi x86 arm64 x86_64].freeze
|
10
|
+
#
|
11
|
+
# Return a structure describing the options.
|
12
|
+
#
|
13
|
+
def self.parse(args)
|
14
|
+
# The options specified on the command line will be collected in *options*.
|
15
|
+
# We set default values here.
|
16
|
+
opt_parser = ParseArgs.new(OpenStruct.new(sym_dir: Dir.pwd))
|
17
|
+
options = opt_parser.parse(args)
|
18
|
+
check(options)
|
19
|
+
check_ios(options)
|
20
|
+
options
|
21
|
+
rescue OptionParser::MissingArgument => x
|
22
|
+
puts ["Error occurred: #{x.message}", '', opt_parser.help].join("\n")
|
23
|
+
exit 2
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.check_ios(options)
|
27
|
+
return unless options.platform == :ios
|
28
|
+
{
|
29
|
+
'address' => options.address,
|
30
|
+
'modulename' => options.modulename
|
31
|
+
}.each { |arg, check| raise(OptionParser::MissingArgument, arg) unless check }
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.check(options)
|
35
|
+
{
|
36
|
+
'platform' => options.platform,
|
37
|
+
'arch' => options.arch
|
38
|
+
}.each { |arg, check| raise(OptionParser::MissingArgument, arg) unless check }
|
39
|
+
end
|
40
|
+
|
41
|
+
def initialize(defaults)
|
42
|
+
@options = defaults
|
43
|
+
@opt_parser = OptionParser.new do |opts|
|
44
|
+
opt_banner(opts)
|
45
|
+
opts.separator ''
|
46
|
+
opt_common(opts)
|
47
|
+
opts.separator ''
|
48
|
+
opt_ios(opts)
|
49
|
+
opts.separator ''
|
50
|
+
opt_default(opts)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def parse(args)
|
55
|
+
@opt_parser.parse!(args)
|
56
|
+
@options
|
57
|
+
end
|
58
|
+
|
59
|
+
def help
|
60
|
+
@opt_parser.help
|
61
|
+
end
|
62
|
+
|
63
|
+
def opt_banner(opts)
|
64
|
+
opts.banner = 'Usage: tracetool OPTION... [FILE]...'
|
65
|
+
opts.separator ''
|
66
|
+
opts.separator 'Examples:'
|
67
|
+
opts.separator "\techo '<<<...>>>' | tracetool --arch armeabi-v7a --platform android --symbols %build_dir%"
|
68
|
+
opts.separator "\ttracetool --platform ios --arch arm64 --address 0x100038000 --module ZombieCastaways dump"
|
69
|
+
end
|
70
|
+
|
71
|
+
def opt_common(opts)
|
72
|
+
opts.separator 'Common options:'
|
73
|
+
# Specify arch
|
74
|
+
opts.on('--arch ARCH', ARCH_LIST, "Specify arch #{ARCH_LIST.join(', ')}") do |arch|
|
75
|
+
@options.arch = arch
|
76
|
+
end
|
77
|
+
|
78
|
+
# Specify platform
|
79
|
+
opts.on('--platform PLATORM', %i[android ios], 'Specify platform (android, ios)') do |platform|
|
80
|
+
@options.platform = platform
|
81
|
+
end
|
82
|
+
|
83
|
+
# Symbols dir
|
84
|
+
opts.on('--symbols [SYMBOLS]', 'Symbols dir. Using current working dir if not specified') do |dir|
|
85
|
+
@options.sym_dir = dir
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def opt_ios(opts)
|
90
|
+
opts.separator 'IOS specific options'
|
91
|
+
# Addres
|
92
|
+
opts.on('--address ADDRESS', 'Baseline address for ios builds') do |address|
|
93
|
+
@options.address = address
|
94
|
+
end
|
95
|
+
|
96
|
+
# Modulename -- execution entry point
|
97
|
+
opts.on('--module MODULENAME', 'Entry point for ios builds') do |modulename|
|
98
|
+
@options.modulename = modulename
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def opt_default(opts)
|
103
|
+
opts.separator 'Base options:'
|
104
|
+
# No argument, shows at tail. This will print an options summary.
|
105
|
+
# Try it and see!
|
106
|
+
opts.on_tail('-h', '--help', 'Show this message') do
|
107
|
+
puts opts
|
108
|
+
exit
|
109
|
+
end
|
110
|
+
|
111
|
+
# Another typical switch to print the version.
|
112
|
+
opts.on_tail('--version', 'Show version') do
|
113
|
+
puts 'tracetool ' + Tracetool::Version.to_s
|
114
|
+
exit
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Tracetool
|
2
|
+
# Utility methods for working with environment
|
3
|
+
module Env
|
4
|
+
class << self
|
5
|
+
# Finds executable in path
|
6
|
+
def which(cmd)
|
7
|
+
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
|
8
|
+
candidates = ENV['PATH'].split(File::PATH_SEPARATOR).flat_map do |path|
|
9
|
+
exts.map { |ext| File.join(path, "#{cmd}#{ext}") }
|
10
|
+
end
|
11
|
+
|
12
|
+
candidates.find { |exe| File.executable?(exe) && !File.directory?(exe) }
|
13
|
+
end
|
14
|
+
|
15
|
+
# Raises exception if can't find executable in path
|
16
|
+
# @raise [ArgumentError] if executable not found
|
17
|
+
def ensure_command(cmd)
|
18
|
+
raise(ArgumentError, "#{cmd} not found in PATH") unless which(cmd)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Checks if `ndk-stack` can be found in path
|
22
|
+
# @raise [ArgumentError] if can't find ndk-stack
|
23
|
+
def ensure_ndk_stack
|
24
|
+
ensure_command('ndk-stack')
|
25
|
+
end
|
26
|
+
|
27
|
+
# Checks if `atos` can be found in path
|
28
|
+
# @raise [ArgumentError] if can't find atos
|
29
|
+
def ensure_atos
|
30
|
+
ensure_command('atos')
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
module Tracetool
|
2
|
+
# Base trace parser logic
|
3
|
+
class BaseTraceParser
|
4
|
+
attr_reader :entry_pattern, :call_pattern
|
5
|
+
|
6
|
+
def initialize(entry_pattern, call_pattern, build_files, convert_numbers = false)
|
7
|
+
@build_files = build_files
|
8
|
+
@entry_pattern = entry_pattern
|
9
|
+
@call_pattern = call_pattern
|
10
|
+
@convert_numbers = convert_numbers
|
11
|
+
end
|
12
|
+
|
13
|
+
# Parse crash dump
|
14
|
+
# Each line should be parsed in Hash which SHOULD or MAY contains following
|
15
|
+
# entries:
|
16
|
+
#
|
17
|
+
# * SHOULD contain `orig` entry -- original trace entry
|
18
|
+
# * MAY contain following entries IF original entry matched pattern:
|
19
|
+
# ** `frame` - stack frame index
|
20
|
+
# ** `lib` - shared library name (android) or module name (ios)
|
21
|
+
# ** `call_description` - original call block
|
22
|
+
# ** MAY contain `call` entry if `call_description` was recognized:
|
23
|
+
# *** `method` - namespaced method name (with class and its namespace)
|
24
|
+
# *** `file` - file path relative to `symbols dir`
|
25
|
+
# *** `line` - line number in specified file
|
26
|
+
def parse(lines)
|
27
|
+
lines
|
28
|
+
.split("\n")
|
29
|
+
.select { |line| line_filter(line) }
|
30
|
+
.map { |line| scan_call(scan_line(line)) }
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
# Will drop other lines from unpacked result
|
36
|
+
def line_filter(line)
|
37
|
+
entry_pattern.match(line)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Find basic entry elements:
|
41
|
+
# * frame
|
42
|
+
# * address
|
43
|
+
# * library
|
44
|
+
# * call description
|
45
|
+
def scan_line(line)
|
46
|
+
e = { orig: line }
|
47
|
+
entry_pattern.match(line) do |m|
|
48
|
+
e.update(extract_groups(m))
|
49
|
+
end
|
50
|
+
e
|
51
|
+
end
|
52
|
+
|
53
|
+
# Parse call description to extract:
|
54
|
+
# * method
|
55
|
+
# * file
|
56
|
+
# * line number
|
57
|
+
def scan_call(e)
|
58
|
+
if e[:call_description]
|
59
|
+
call_pattern.match(e[:call_description]) do |m|
|
60
|
+
call = extract_groups(m)
|
61
|
+
# Update file entry with expanded path
|
62
|
+
call[:file] = find_file(call[:file]) if call[:file]
|
63
|
+
|
64
|
+
e[:call] = call
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
e
|
69
|
+
end
|
70
|
+
|
71
|
+
# Find file with specified file name in symbols dir
|
72
|
+
# Can return multiple files if name was ambigous
|
73
|
+
def find_file(file)
|
74
|
+
# Find all matching files
|
75
|
+
# remove build_dir from path
|
76
|
+
# remove leading '/'
|
77
|
+
glob = File.join('**', File.basename(file))
|
78
|
+
files = @build_files.select { |f| File.fnmatch(glob, f) }
|
79
|
+
|
80
|
+
# If has only option return first
|
81
|
+
return files.first if files.size == 1
|
82
|
+
# Return original file if files empty
|
83
|
+
return file if files.empty?
|
84
|
+
|
85
|
+
# If got ambiguous files return all
|
86
|
+
files
|
87
|
+
end
|
88
|
+
|
89
|
+
def extract_groups(match)
|
90
|
+
groups = match
|
91
|
+
.names
|
92
|
+
.map { |name| [name.to_sym, match[name]] }
|
93
|
+
.flat_map { |name, value| [name, try_convert(value)] }
|
94
|
+
Hash[*groups]
|
95
|
+
end
|
96
|
+
|
97
|
+
def try_convert(value)
|
98
|
+
return value unless @convert_numbers
|
99
|
+
return value.to_i if /^\d+$/ =~ value
|
100
|
+
|
101
|
+
value
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'open3'
|
2
|
+
module Tracetool
|
3
|
+
# helper module for launching commands
|
4
|
+
module Pipe
|
5
|
+
# Executes shell command
|
6
|
+
class Executor
|
7
|
+
def initialize(cmd, *args)
|
8
|
+
@cmd = cmd
|
9
|
+
@args = args
|
10
|
+
end
|
11
|
+
|
12
|
+
def cmd
|
13
|
+
[@cmd, @args].flatten
|
14
|
+
end
|
15
|
+
|
16
|
+
def <<(args)
|
17
|
+
args = args.join("\n") if args.is_a? Array
|
18
|
+
IO.popen(cmd, 'r+') do |io|
|
19
|
+
io.write(args)
|
20
|
+
io.close_write
|
21
|
+
io.read.chomp
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class << self
|
27
|
+
def[](cmd, *args)
|
28
|
+
Executor.new(cmd, args)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/tracetool.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
require 'powerpack/string'
|
3
|
+
|
4
|
+
require_relative 'version'
|
5
|
+
|
6
|
+
require_relative 'tracetool/android'
|
7
|
+
require_relative 'tracetool/ios'
|
8
|
+
require_relative 'tracetool/utils/cli'
|
9
|
+
require_relative 'tracetool/utils/env'
|
10
|
+
require_relative 'tracetool/utils/pipe'
|
11
|
+
|
12
|
+
opts = Tracetool::ParseArgs.parse(ARGV)
|
13
|
+
|
14
|
+
case opts.platform
|
15
|
+
when :android
|
16
|
+
stack = ARGF.read
|
17
|
+
puts Tracetool::Android.scan(stack,
|
18
|
+
symbols: opts.sym_dir,
|
19
|
+
arch: opts.arch.to_s)
|
20
|
+
when :ios
|
21
|
+
stack = ARGF.read
|
22
|
+
puts Tracetool::IOS.scan(stack,
|
23
|
+
arch: opts.arch.to_s,
|
24
|
+
xarchive: opts.sym_dir,
|
25
|
+
module_name: opts.modulename,
|
26
|
+
load_address: opts.address)
|
27
|
+
else
|
28
|
+
raise "Unknown(#{opts.platform})"
|
29
|
+
end
|
data/lib/version.rb
ADDED
metadata
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tracetool
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.3.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- ilya.arkhanhelsky
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-11-12 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: powerpack
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.1.1
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.1.1
|
27
|
+
description: Helper for unpacking Android and iOS traces
|
28
|
+
email: ilya.arkhanhelsky@vizor-games.com
|
29
|
+
executables:
|
30
|
+
- tracetool
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- bin/tracetool
|
35
|
+
- lib/tracetool.rb
|
36
|
+
- lib/tracetool/android.rb
|
37
|
+
- lib/tracetool/android/java.rb
|
38
|
+
- lib/tracetool/android/native.rb
|
39
|
+
- lib/tracetool/ios.rb
|
40
|
+
- lib/tracetool/ios/parser.rb
|
41
|
+
- lib/tracetool/ios/scanner.rb
|
42
|
+
- lib/tracetool/utils/cli.rb
|
43
|
+
- lib/tracetool/utils/env.rb
|
44
|
+
- lib/tracetool/utils/parser.rb
|
45
|
+
- lib/tracetool/utils/pipe.rb
|
46
|
+
- lib/version.rb
|
47
|
+
homepage: https://github.com/vizor-games/tracetool
|
48
|
+
licenses:
|
49
|
+
- MIT
|
50
|
+
metadata: {}
|
51
|
+
post_install_message:
|
52
|
+
rdoc_options: []
|
53
|
+
require_paths:
|
54
|
+
- lib
|
55
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '0'
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: '0'
|
65
|
+
requirements: []
|
66
|
+
rubyforge_project:
|
67
|
+
rubygems_version: 2.6.13
|
68
|
+
signing_key:
|
69
|
+
specification_version: 4
|
70
|
+
summary: Tracetool
|
71
|
+
test_files: []
|