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 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,2 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative '../lib/tracetool'
@@ -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,11 @@
1
+ require_relative 'ios/scanner'
2
+ require_relative 'ios/parser'
3
+
4
+ module Tracetool
5
+ # IOS trace unpacking routines
6
+ module IOS
7
+ def self.scan(string, opts)
8
+ IOSTraceScanner.new.process(string, OpenStruct.new(opts))
9
+ end
10
+ end
11
+ 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
@@ -0,0 +1,13 @@
1
+ module Tracetool
2
+ # Version constant
3
+ module Version
4
+ VERSION = [0, 3, 0].freeze
5
+
6
+ class << self
7
+ # @return [String] version string
8
+ def to_s
9
+ VERSION.join('.')
10
+ end
11
+ end
12
+ end
13
+ end
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: []