tracetool 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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: []
|