tracetool 0.4.0 → 0.5.2

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 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