tracetool 0.4.0 → 0.5.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 28053aff3e16e9baee1198e08bdbeffdefeef2f0
4
- data.tar.gz: 894ac01ee7333ee6b02dc5fb422114f6269a0a72
2
+ SHA256:
3
+ metadata.gz: 7b0baff5d5f633d2474acf70b639fad429dd09b5b4cc4f80333ef55f72c1e5d8
4
+ data.tar.gz: 2851ab0c1b575429400937ad889b0a06fdce6f9ebaf550b05956b7879166d6f6
5
5
  SHA512:
6
- metadata.gz: 3fa1e1578d5c6802e508cea8b7c1af90ff2ef4f83f3574b7cf0b218afbcc48c47783706bec06273a1a6887470ebddd0ccbd15fb0448d921296033ce174b064d1
7
- data.tar.gz: 338ed267bce0c0d91ecdb47cdfe6d1f8eeed36179717bc16a6c5362c84fb15d5f3099906e35ad0eaa90af7368c7e8fd2bd1c5bcb1d66b69bcb00265c4330874d
6
+ metadata.gz: ba21b749c8875fec7e26c70da2899dfb89ea5c0fcfed2a7fc1233b34ee222d5de43cf630fc13998cf3e95335eee152e75e7fdfea8e998867ccfbd3c581eb3a1b
7
+ data.tar.gz: 7f0236c1c234fa42c8fc68874cbc63e0271c773078c64cf50a5bea4e1d6951757affb02064360590c5668e559633e85b6a237d6dff7d901edfe23fbf7a522888
@@ -16,6 +16,7 @@ module Tracetool
16
16
  # Find scanner which matches trace format
17
17
  @scanner = SCANNERS.map { |s| s[trace] }.compact.first
18
18
  raise(ArgumentError, "#{trace}\n not android trace?") unless @scanner
19
+
19
20
  @scanner.process(context)
20
21
  end
21
22
 
@@ -25,6 +26,7 @@ module Tracetool
25
26
  # Or `nil`. If there was no scanning.
26
27
  def parser(files)
27
28
  return unless @scanner
29
+
28
30
  @scanner.parser(files)
29
31
  end
30
32
  end
@@ -5,9 +5,9 @@ module Tracetool
5
5
  # Parses java stack traces
6
6
  class JavaTraceParser < Tracetool::BaseTraceParser
7
7
  # Describes java stack entry
8
- STACK_ENTRY_PATTERN = /^(\s+at (?<call_description>.+))|((?<error>.+?): (?<message>.+))$/
8
+ STACK_ENTRY_PATTERN = /^(\s+at (?<call_description>.+))|((?<error>.+?): (?<message>.+))$/.freeze
9
9
  # Describes java method call
10
- CALL_PATTERN = /(?<class>.+)\.(?<method>[^\(]+)\((((?<file>.+\.java):(?<line>\d+))|(?<location>.+))\)$/
10
+ CALL_PATTERN = /(?<class>.+)\.(?<method>[^\(]+)\((((?<file>.+\.java):(?<line>\d+))|(?<location>.+))\)$/.freeze
11
11
 
12
12
  def initialize(files)
13
13
  super(STACK_ENTRY_PATTERN, CALL_PATTERN, files, true)
@@ -15,8 +15,14 @@ module Tracetool
15
15
  end
16
16
  # Processes java traces
17
17
  class JavaTraceScanner
18
- RX_FIRST_EXCEPTION_LINE = /^.+$/
19
- RX_OTHER_EXCEPTION_LINE = /at [^(]+\(([^:]+:\d+)|(Native Method)\)$/
18
+ # Usually java trace starts with
19
+ # com.something.SomeClass(: Some message)?
20
+ RX_FIRST_EXCEPTION_LINE = /^([a-zA-Z.]*)(:.*)?$/.freeze
21
+
22
+ # Rest is expanded as
23
+ # at com.other.OtherClass.someMethod(OtherClass.java:42)
24
+ # Source marker can be just "Native Method" or "Unknown Source"
25
+ RX_OTHER_EXCEPTION_LINE = /((at [a-zA-Z$.]+)|(Caused by:)|(\.\.\. [0-9]* more))(.+)?$/.freeze
20
26
 
21
27
  def initialize(string)
22
28
  @trace = string
@@ -38,8 +44,6 @@ module Tracetool
38
44
  def match(string)
39
45
  # Split into lines
40
46
  first, *rest = string.split("\n")
41
-
42
- return if rest.nil? || rest.empty?
43
47
  return unless RX_FIRST_EXCEPTION_LINE.match(first)
44
48
 
45
49
  rest.all? { |line| RX_OTHER_EXCEPTION_LINE.match(line) }
@@ -5,10 +5,15 @@ module Tracetool
5
5
  # Android traces scanner and mapper
6
6
  class NativeTraceParser < Tracetool::BaseTraceParser
7
7
  # Describes android stack entry
8
+ # rubocop:disable Metrics/LineLength
8
9
  STACK_ENTRY_PATTERN =
9
- %r{Stack frame #(?<frame>\d+) (?<address>\w+ [a-f\d]+) (?<lib>[/\w\d\.-]+)( )?(: (?<call_description>.+))?$}
10
+ %r{Stack frame #(?<frame>\d+) (?<address>\w+ [a-f\d]+) (?<lib>[/\w\d\._!=-]+)( )?(:? (?<call_description>.+))?$}.freeze
11
+ # rubocop:enable Metrics/LineLength
10
12
  # Describes android native method call (class::method and source file with line number)
11
- CALL_PATTERN = /(Routine )?(?<method>.+) ((in)|(at)) (?<file>.+):(?<line>\d+)/
13
+ CALL_PATTERN = [
14
+ /((Routine )?(?<method>.+) ((in)|(at)) (?<file>.+):(?<line>\d+))/,
15
+ /(?<method>.+?) \d+/
16
+ ].freeze
12
17
 
13
18
  def initialize(files)
14
19
  super(STACK_ENTRY_PATTERN, CALL_PATTERN, files, true)
@@ -30,7 +35,9 @@ module Tracetool
30
35
  # @param [String] trace packed stack trace
31
36
  # @return well formed stack trace
32
37
  def unpack(trace)
33
- dump_body = prepare(trace).map.with_index { |line, index| convert_line(line, index) }
38
+ dump_body = prepare(trace)
39
+ .map
40
+ .with_index { |line, index| convert_line(line, index) }
34
41
  add_header(dump_body.join("\n"))
35
42
  end
36
43
 
@@ -48,8 +55,9 @@ module Tracetool
48
55
  def convert_line(line, index)
49
56
  frame = index
50
57
  addr = line[/^(-?\d+) (.*)$/, 1]
51
- lib = line[/^(-?\d+) (.*)$/, 2].strip
52
- ' #%02i pc %08x %s'.format(frame, addr, lib)
58
+ lib = (line[/^(-?\d+) (.*)$/, 2] || '').strip # nil safe
59
+ ' #%02<frame>i pc %08<addr>x %<lib>s'
60
+ .format(frame: frame, addr: addr, lib: lib)
53
61
  end
54
62
 
55
63
  # If needed here we'll drop all unneeded leading characters from each
@@ -63,14 +71,15 @@ module Tracetool
63
71
  # Processes native traces
64
72
  class NativeTraceScanner
65
73
  # Initial sequence of asterisks which marks begining of trace body
66
- TRACE_DELIMETER = '*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***'.freeze
67
- RX_INITIAL_ASTERISKS = /#{TRACE_DELIMETER.gsub('*', '\*')}/
74
+ TRACE_DELIMETER =
75
+ '*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***'.freeze
76
+ RX_INITIAL_ASTERISKS = /#{TRACE_DELIMETER.gsub('*', '\*')}/.freeze
68
77
  # Contains address line like
69
78
  #
70
79
  # ```
71
80
  # pc 00000000004321ec libfoo.so
72
81
  # ```
73
- RX_PC_ADDRESS = /pc \d+/
82
+ RX_PC_ADDRESS = /pc \d+/.freeze
74
83
 
75
84
  # Format of packed trace.
76
85
  # Consists of one or more trace blocks.
@@ -84,7 +93,7 @@ module Tracetool
84
93
  # ** symbol offset `/\d+/`
85
94
  #
86
95
  # Last two entries can be missing.
87
- RX_PACKED_FORMAT = /^(<<<(\d+ [^ ]+ ([^ ]+ \d+)?;)+>>>)+$/
96
+ RX_PACKED_FORMAT = /^(<<<([-?\d]+ [^ ]+ (.+)?;)+>>>)+$/.freeze
88
97
 
89
98
  # @param [String] string well formed native android stack trace
90
99
  # @see https://developer.android.com/ndk/guides/ndk-stack.html
@@ -96,13 +105,7 @@ module Tracetool
96
105
  # path to symbols dir
97
106
  # @return [String] desymbolicated stack trace
98
107
  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
108
+ Pipe['ndk-stack', '-sym', ctx.symbols] << @trace
106
109
  end
107
110
 
108
111
  # Create parser for current trace format
@@ -1,3 +1,4 @@
1
+ require_relative 'ios/atos_context'
1
2
  require_relative 'ios/scanner'
2
3
  require_relative 'ios/parser'
3
4
 
@@ -0,0 +1,35 @@
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
+ def initialize(ctx)
12
+ check_arguments(ctx)
13
+ @load_address = ctx.load_address
14
+ @binary_path = module_binary(ctx.xarchive, ctx.module_name)
15
+ @arch = ctx.arch || 'arm64'
16
+ end
17
+
18
+ def to_args
19
+ %w[-o -l -arch].zip([@binary_path, @load_address, @arch]).flatten
20
+ end
21
+
22
+ private
23
+
24
+ def module_binary(xarchive, module_name)
25
+ File.join(xarchive, 'dSYMs', "#{module_name}.app.dSYM", 'Contents', 'Resources', 'DWARF', module_name)
26
+ end
27
+
28
+ def check_arguments(ctx)
29
+ REQUIRED_ARGUMENTS.each do |a|
30
+ ctx[a] || raise(ArgumentError, "Missing `#{a}` value")
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -5,10 +5,11 @@ module Tracetool
5
5
  # IOS traces scanner and source mapper
6
6
  class IOSTraceParser < Tracetool::BaseTraceParser
7
7
  # Describes IOS stack entry
8
- STACK_ENTRY_PATTERN = /^(?<frame>\d+) (?<binary>[^ ]+) (?<call_description>.+)$/
8
+ STACK_ENTRY_PATTERN = /^(?<frame>\d+) (?<binary>[^ ]+) (?<call_description>.+)$/.freeze
9
9
  # Describes source block
10
10
  SOURCE_PATTERN =
11
11
  /^((-?\[(?<class>[^ ]+) (?<method>.+)\])|(?<method>.+)) \(in (?<module>.+)\) \((?<file>.+):(?<line>\d+)\)$/
12
+ .freeze
12
13
 
13
14
  def initialize(files)
14
15
  super(STACK_ENTRY_PATTERN, SOURCE_PATTERN, files, true)
@@ -1,38 +1,5 @@
1
1
  module Tracetool
2
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
3
  # launches atos
37
4
  class IOSTraceScanner
38
5
  # Stack trace line consists of numerous whitespace separated
@@ -45,10 +12,29 @@ module Tracetool
45
12
  # @return [Array] containing (%binary_name%, %address%) pairs
46
13
  def parse(trace)
47
14
  trace.split("\n").map do |line|
48
- line.split(' ')[1..2] # Fetch binary name and address
15
+ parse_line(line)
49
16
  end
50
17
  end
51
18
 
19
+ # Parse trace line from trace. Which usualy looks like this:
20
+ # 3 My Module Name 0x0000000102d6e9f4 My Module Name + 5859828
21
+ # We need to fetch two values: 'My Module Name' and '0x0000000102d6e9f4'.
22
+ def parse_line(line)
23
+ parts = line.split(' ')
24
+ parts.shift # Frame number, not needed
25
+
26
+ module_name = ''
27
+
28
+ until parts.first.start_with?('0x')
29
+ module_name += parts.shift
30
+ module_name += ' '
31
+ end
32
+
33
+ address = parts.shift
34
+
35
+ [module_name.chop, address]
36
+ end
37
+
52
38
  def process(trace, context)
53
39
  trace = parse(trace)
54
40
  desym = run_atos(context, trace.map(&:last))
@@ -6,8 +6,8 @@ require_relative '../../version'
6
6
  module Tracetool
7
7
  # Tracetool cli args parser
8
8
  class ParseArgs
9
- # List of supported abis
10
- ARCH_LIST = %i[armeabi-v7a armeabi x86 arm64 x86_64].freeze
9
+ # List of supported abis. Only needed for iOS unpacking
10
+ ARCH_LIST = %i[arm arm64].freeze
11
11
  #
12
12
  # Return a structure describing the options.
13
13
  #
@@ -19,24 +19,25 @@ module Tracetool
19
19
  check(options)
20
20
  check_ios(options)
21
21
  options
22
- rescue OptionParser::MissingArgument => x
23
- io.write ["Error occurred: #{x.message}", '', opt_parser.help].join("\n")
22
+ rescue OptionParser::MissingArgument => e
23
+ io.write ["Error occurred: #{e.message}", '', opt_parser.help].join("\n")
24
24
  io.write "\n"
25
- raise(x)
25
+ raise(e)
26
26
  end
27
27
 
28
28
  def self.check_ios(options)
29
29
  return unless options.platform == :ios
30
+
30
31
  {
31
32
  'address' => options.address,
32
- 'module' => options.modulename
33
+ 'module' => options.modulename,
34
+ 'arch' => options.arch
33
35
  }.each { |arg, check| raise(OptionParser::MissingArgument, arg) unless check }
34
36
  end
35
37
 
36
38
  def self.check(options)
37
39
  {
38
- 'platform' => options.platform,
39
- 'arch' => options.arch
40
+ 'platform' => options.platform
40
41
  }.each { |arg, check| raise(OptionParser::MissingArgument, arg) unless check }
41
42
  end
42
43
 
@@ -1,12 +1,16 @@
1
+ require_relative 'string'
2
+
1
3
  module Tracetool
2
4
  # Base trace parser logic
3
5
  class BaseTraceParser
6
+ include StringUtils
7
+
4
8
  attr_reader :entry_pattern, :call_pattern
5
9
 
6
10
  def initialize(entry_pattern, call_pattern, build_files, convert_numbers = false)
7
11
  @build_files = build_files
8
12
  @entry_pattern = entry_pattern
9
- @call_pattern = call_pattern
13
+ @call_pattern = call_pattern.is_a?(Array) ? call_pattern : [call_pattern]
10
14
  @convert_numbers = convert_numbers
11
15
  end
12
16
 
@@ -54,36 +58,63 @@ module Tracetool
54
58
  # * method
55
59
  # * file
56
60
  # * 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
61
+ def scan_call(call_info)
62
+ call_description = call_info[:call_description]
63
+ # TODO: Lazy check
64
+ match = call_description && call_pattern.map { |p| p.match(call_description) }.compact.first
65
+ if match
66
+ call = extract_groups(match)
67
+ # Update file entry with expanded path
68
+ call[:file] = find_file(call[:file]) if call[:file]
69
+
70
+ call_info[:call] = call
66
71
  end
67
72
 
68
- e
73
+ call_info
69
74
  end
70
75
 
71
76
  # Find file with specified file name in symbols dir
72
- # Can return multiple files if name was ambigous
77
+ # Can return multiple files if name was ambiguous
73
78
  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
+ file_name = File.basename(file)
80
+ # Firstly we'll drop obvious mismatches where basename of file differs
81
+ candidates = @build_files.select { |path| File.basename(path) == file_name }
82
+ # In case when got ambiguous files return all try to find closest match
83
+ files = find_closest_files(file, candidates)
79
84
 
80
85
  # If has only option return first
81
86
  return files.first if files.size == 1
82
87
  # Return original file if files empty
83
88
  return file if files.empty?
84
89
 
85
- # If got ambiguous files return all
86
- files
90
+ files # Return all files if many matched
91
+ end
92
+
93
+ # Select from candidates list such files
94
+ # that ends with maximum substring of file
95
+ # @param [String] file file path to match
96
+ # @param [Array<String>] candidates list of candidates path
97
+ # @return [Array<String>] list of files with maximum length matches
98
+ def find_closest_files(file, candidates)
99
+ candidates.inject([[], 0]) do |acc, elem|
100
+ # Current element score is length of longest common postfix
101
+ elem_score = file.longest_common_postfix(elem).length
102
+
103
+ # Unpack accumulator as (list_of_matched_files, max_score)
104
+ matched, score = acc
105
+ # Will update if only have better score
106
+ if elem_score >= score
107
+ # Current score more than last known score, so now
108
+ # we drop all previous results and replace them with
109
+ # current element
110
+ matched = [] if elem_score > score
111
+ score = elem_score
112
+ # Update list of matched
113
+ matched << elem
114
+ end
115
+
116
+ [matched, score]
117
+ end.first
87
118
  end
88
119
 
89
120
  def extract_groups(match)
@@ -1,6 +1,7 @@
1
1
  require 'open3'
2
+
2
3
  module Tracetool
3
- # helper module for launching commands
4
+ # Helper module for launching commands
4
5
  module Pipe
5
6
  # Executes shell command
6
7
  class Executor
@@ -14,12 +15,10 @@ module Tracetool
14
15
  end
15
16
 
16
17
  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
18
+ out, err, status = Open3.capture3({}, *cmd, stdin_data: args)
19
+ raise "#{cmd.join(' ')} (exit: #{status.exitstatus}) #{err.chomp}" unless status.success?
20
+
21
+ out.chomp
23
22
  end
24
23
  end
25
24
 
@@ -0,0 +1,24 @@
1
+ module Tracetool
2
+ # Set of utility methods for working with strings
3
+ module StringUtils
4
+ # Extended string class
5
+ # rubocop:disable Style/ClassAndModuleChildren
6
+ class ::String
7
+ # Return longest common postfix
8
+ # @param [String] other other string to match
9
+ # @return [String] longest common postfix
10
+ def longest_common_postfix(other)
11
+ sidx = length - 1
12
+ oidx = other.length - 1
13
+
14
+ while sidx >= 0 && oidx >= 0 && (self[sidx] == other[oidx])
15
+ sidx -= 1
16
+ oidx -= 1
17
+ end
18
+
19
+ other[(oidx + 1)..-1]
20
+ end
21
+ end
22
+ # rubocop:enable Style/ClassAndModuleChildren
23
+ end
24
+ end
@@ -1,7 +1,7 @@
1
1
  module Tracetool
2
2
  # Version constant
3
3
  module Version
4
- VERSION = [0, 4, 0].freeze
4
+ VERSION = [0, 5, 2].freeze
5
5
 
6
6
  class << self
7
7
  # @return [String] version string
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tracetool
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - ilya.arkhanhelsky
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-11-27 00:00:00.000000000 Z
11
+ date: 2020-10-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: powerpack
@@ -37,12 +37,14 @@ files:
37
37
  - lib/tracetool/android/java.rb
38
38
  - lib/tracetool/android/native.rb
39
39
  - lib/tracetool/ios.rb
40
+ - lib/tracetool/ios/atos_context.rb
40
41
  - lib/tracetool/ios/parser.rb
41
42
  - lib/tracetool/ios/scanner.rb
42
43
  - lib/tracetool/utils/cli.rb
43
44
  - lib/tracetool/utils/env.rb
44
45
  - lib/tracetool/utils/parser.rb
45
46
  - lib/tracetool/utils/pipe.rb
47
+ - lib/tracetool/utils/string.rb
46
48
  - lib/tracetool_cli.rb
47
49
  - lib/version.rb
48
50
  homepage: https://github.com/vizor-games/tracetool
@@ -64,8 +66,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
64
66
  - !ruby/object:Gem::Version
65
67
  version: '0'
66
68
  requirements: []
67
- rubyforge_project:
68
- rubygems_version: 2.6.13
69
+ rubygems_version: 3.1.2
69
70
  signing_key:
70
71
  specification_version: 4
71
72
  summary: Tracetool