appmap 0.45.0 → 0.45.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +29 -0
- data/lib/appmap/config.rb +112 -76
- data/lib/appmap/hook.rb +106 -70
- data/lib/appmap/util.rb +8 -0
- data/lib/appmap/version.rb +1 -1
- data/spec/abstract_controller_base_spec.rb +1 -1
- data/spec/hook_spec.rb +7 -3
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b91b79723565f45d9d59ce341a9944a3a4e1c853bc77c1b176dc7b26ede9df22
|
4
|
+
data.tar.gz: f9be88ae7e83b801f66ada4087a06923ad9fa4d10e498f1c8b07fe0e301552d7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e7eda447c67a44ad10226f5b6e9eb51c82995a4b2df970af27b0e2b5304101d419a6b3843ef6f0a983a3f952da2e3ed55f0de327b2856fcf970961c410c19b79
|
7
|
+
data.tar.gz: 839df6608d503e74f663d42673d9cf91866592a1715cee1c828c3de23ae472fdff6025ea9ee767a33e384b2ed51f46a999001d354f3e763deba46a5253eb5661
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
## [0.45.1](https://github.com/applandinc/appmap-ruby/compare/v0.45.0...v0.45.1) (2021-05-04)
|
2
|
+
|
3
|
+
|
4
|
+
### Bug Fixes
|
5
|
+
|
6
|
+
* Optimize instrumentation and load time ([db4a8ce](https://github.com/applandinc/appmap-ruby/commit/db4a8ceed4103a52caafa46626c66f33fbfeac27))
|
7
|
+
|
1
8
|
# [0.45.0](https://github.com/applandinc/appmap-ruby/compare/v0.44.0...v0.45.0) (2021-05-03)
|
2
9
|
|
3
10
|
|
data/README.md
CHANGED
@@ -14,6 +14,7 @@
|
|
14
14
|
- [AppMap Swagger](#appmap-swagger)
|
15
15
|
- [Uploading AppMaps](#uploading-appmaps)
|
16
16
|
- [Development](#development)
|
17
|
+
- [Internal architecture](#internal-architecture)
|
17
18
|
- [Running tests](#running-tests)
|
18
19
|
- [Using fixture apps](#using-fixture-apps)
|
19
20
|
- [`test/fixtures`](#testfixtures)
|
@@ -369,6 +370,34 @@ For instructions on uploading, see the documentation of the [AppLand CLI](https:
|
|
369
370
|
# Development
|
370
371
|
[![Build Status](https://travis-ci.com/applandinc/appmap-ruby.svg?branch=master)](https://travis-ci.com/applandinc/appmap-ruby)
|
371
372
|
|
373
|
+
## Internal architecture
|
374
|
+
|
375
|
+
**Configuration**
|
376
|
+
|
377
|
+
*appmap.yml* is loaded into an `AppMap::Config`.
|
378
|
+
|
379
|
+
**Hooking**
|
380
|
+
|
381
|
+
Once configuration is loaded, `AppMap::Hook` is enabled. "Hooking" refers to the process of replacing a method
|
382
|
+
with a "hooked" version of the method. The hooked method checks to see if tracing is enabled. If so, it wraps the original
|
383
|
+
method with calls that record the parameters and return value.
|
384
|
+
|
385
|
+
**Builtins**
|
386
|
+
|
387
|
+
`Hook` begins by iterating over builtin classes and modules defined in the `Config`. Builtins include code
|
388
|
+
like `openssl` and `net/http`. This code is not dependent on any external libraries being present, and
|
389
|
+
`appmap` cannot guarantee that it will be loaded before builtins. Therefore, it's necessary to require it and
|
390
|
+
hook it by looking up the classes and modules as constants in the `Object` namespace.
|
391
|
+
|
392
|
+
**User code and gems**
|
393
|
+
|
394
|
+
After hooking builtins, `Hook` attaches a [TracePoint](https://ruby-doc.org/core-2.6/TracePoint.html) to `:begin` events.
|
395
|
+
This TracePoint is notified each time a new class or module is being evaluated. When this happens, `Hook` uses the `Config`
|
396
|
+
to determine whether any code within the evaluated file is configured for hooking. If so, a `TracePoint` is attached to
|
397
|
+
`:end` events. Each `:end` event is fired when a class or module definition is completed. When this happens, the `Hook` enumerates
|
398
|
+
the public methods of the class or module, hooking the ones that are targeted by the `Config`. Once the `:end` TracePoint leaves
|
399
|
+
the scope of the `:begin`, the `:end` TracePoint is disabled.
|
400
|
+
|
372
401
|
## Running tests
|
373
402
|
|
374
403
|
Before running tests, configure `local.appmap` to point to your local `appmap-ruby` directory.
|
data/lib/appmap/config.rb
CHANGED
@@ -2,7 +2,21 @@
|
|
2
2
|
|
3
3
|
module AppMap
|
4
4
|
class Config
|
5
|
+
# Specifies a code +path+ to be mapped.
|
6
|
+
# Options:
|
7
|
+
#
|
8
|
+
# * +gem+ may indicate a gem name that "owns" the path
|
9
|
+
# * +package_name+ can be used to make sure that the code is required so that it can be loaded. This is generally used with
|
10
|
+
# builtins, or when the path to be required is not automatically required when bundler requires the gem.
|
11
|
+
# * +exclude+ can be used used to exclude sub-paths. Generally not used with +gem+.
|
12
|
+
# * +labels+ is used to apply labels to matching code. This is really only useful when the package will be applied to
|
13
|
+
# specific functions, via TargetMethods.
|
14
|
+
# * +shallow+ indicates shallow mapping, in which only the entrypoint to a gem is recorded.
|
5
15
|
Package = Struct.new(:path, :gem, :package_name, :exclude, :labels, :shallow) do
|
16
|
+
# This is for internal use only.
|
17
|
+
private_methods :gem
|
18
|
+
|
19
|
+
# Specifies the class that will convert code events into event objects.
|
6
20
|
attr_writer :handler_class
|
7
21
|
|
8
22
|
def handler_class
|
@@ -18,25 +32,36 @@ module AppMap
|
|
18
32
|
end
|
19
33
|
|
20
34
|
class << self
|
35
|
+
# Builds a package for a path, such as `app/models` in a Rails app. Generally corresponds to a `path:` entry
|
36
|
+
# in appmap.yml. Also used for mapping specific methods via TargetMethods.
|
21
37
|
def build_from_path(path, shallow: false, package_name: nil, exclude: [], labels: [])
|
22
38
|
Package.new(path, nil, package_name, exclude, labels, shallow)
|
23
39
|
end
|
24
40
|
|
25
|
-
|
26
|
-
|
41
|
+
# Builds a package for gem. Generally corresponds to a `gem:` entry in appmap.yml. Also used when mapping
|
42
|
+
# a builtin.
|
43
|
+
def build_from_gem(gem, shallow: true, package_name: nil, exclude: [], labels: [], optional: false, force: false)
|
44
|
+
if !force && %w[method_source activesupport].member?(gem)
|
27
45
|
warn "WARNING: #{gem} cannot be AppMapped because it is a dependency of the appmap gem"
|
28
46
|
return
|
29
47
|
end
|
30
|
-
|
48
|
+
path = gem_path(gem, optional)
|
49
|
+
if path
|
50
|
+
Package.new(path, gem, package_name, exclude, labels, shallow)
|
51
|
+
else
|
52
|
+
warn "#{gem} is not available in the bundle" if AppMap::Hook::LOG
|
53
|
+
end
|
31
54
|
end
|
32
55
|
|
33
56
|
private_class_method :new
|
34
57
|
|
35
58
|
protected
|
36
59
|
|
37
|
-
def gem_path(gem)
|
38
|
-
gemspec = Gem.loaded_specs[gem]
|
39
|
-
|
60
|
+
def gem_path(gem, optional)
|
61
|
+
gemspec = Gem.loaded_specs[gem]
|
62
|
+
# This exception will notify a user that their appmap.yml contains non-existent gems.
|
63
|
+
raise "Gem #{gem.inspect} not found" unless gemspec || optional
|
64
|
+
gemspec ? gemspec.gem_dir : nil
|
40
65
|
end
|
41
66
|
end
|
42
67
|
|
@@ -57,7 +82,7 @@ module AppMap
|
|
57
82
|
end
|
58
83
|
end
|
59
84
|
|
60
|
-
Function = Struct.new(:package, :cls, :labels, :function_names) do
|
85
|
+
Function = Struct.new(:package, :cls, :labels, :function_names) do # :nodoc:
|
61
86
|
def to_h
|
62
87
|
{
|
63
88
|
package: package,
|
@@ -67,8 +92,9 @@ module AppMap
|
|
67
92
|
}.compact
|
68
93
|
end
|
69
94
|
end
|
95
|
+
private_constant :Function
|
70
96
|
|
71
|
-
class
|
97
|
+
class TargetMethods # :nodoc:
|
72
98
|
attr_reader :method_names, :package
|
73
99
|
|
74
100
|
def initialize(method_names, package)
|
@@ -76,6 +102,10 @@ module AppMap
|
|
76
102
|
@package = package
|
77
103
|
end
|
78
104
|
|
105
|
+
def include_method?(method_name)
|
106
|
+
Array(method_names).include?(method_name)
|
107
|
+
end
|
108
|
+
|
79
109
|
def to_h
|
80
110
|
{
|
81
111
|
package: package.name,
|
@@ -83,55 +113,58 @@ module AppMap
|
|
83
113
|
}
|
84
114
|
end
|
85
115
|
end
|
116
|
+
private_constant :TargetMethods
|
86
117
|
|
87
118
|
OPENSSL_PACKAGES = ->(labels) { Package.build_from_path('openssl', package_name: 'openssl', labels: labels) }
|
88
119
|
|
89
120
|
# Methods that should always be hooked, with their containing
|
90
121
|
# package and labels that should be applied to them.
|
91
122
|
HOOKED_METHODS = {
|
92
|
-
'
|
93
|
-
'
|
94
|
-
'ActionDispatch::
|
95
|
-
'ActionDispatch::Cookies::
|
96
|
-
'
|
97
|
-
'CanCan::
|
98
|
-
'CanCan::Ability' => Hook.new(%i[authorize!], Package.build_from_path('cancancan', labels: %w[security.authorization])),
|
123
|
+
'ActionView::Renderer' => TargetMethods.new(:render, Package.build_from_gem('actionview', package_name: 'action_view', labels: %w[mvc.view], optional: true)),
|
124
|
+
'ActionDispatch::Request::Session' => TargetMethods.new(%i[destroy [] dig values []= clear update delete fetch merge], Package.build_from_gem('actionpack', package_name: 'action_dispatch', labels: %w[http.session], optional: true)),
|
125
|
+
'ActionDispatch::Cookies::CookieJar' => TargetMethods.new(%i[[]= clear update delete recycle], Package.build_from_gem('actionpack', package_name: 'action_dispatch', labels: %w[http.cookie], optional: true)),
|
126
|
+
'ActionDispatch::Cookies::EncryptedCookieJar' => TargetMethods.new(%i[[]=], Package.build_from_gem('actionpack', package_name: 'action_dispatch', labels: %w[http.cookie crypto.encrypt], optional: true)),
|
127
|
+
'CanCan::ControllerAdditions' => TargetMethods.new(%i[authorize! can? cannot?], Package.build_from_gem('cancancan', labels: %w[security.authorization], optional: true)),
|
128
|
+
'CanCan::Ability' => TargetMethods.new(%i[authorize!], Package.build_from_gem('cancancan', labels: %w[security.authorization], optional: true)),
|
99
129
|
'ActionController::Instrumentation' => [
|
100
|
-
|
101
|
-
|
130
|
+
TargetMethods.new(%i[process_action send_file send_data redirect_to], Package.build_from_gem('actionpack', package_name: 'action_controller', labels: %w[mvc.controller], optional: true)),
|
131
|
+
TargetMethods.new(%i[render], Package.build_from_gem('actionpack', package_name: 'action_controller', labels: %w[mvc.view], optional: true)),
|
102
132
|
]
|
103
133
|
}.freeze
|
104
134
|
|
105
135
|
BUILTIN_METHODS = {
|
106
|
-
'OpenSSL::PKey::PKey' =>
|
107
|
-
'OpenSSL::X509::Request' =>
|
108
|
-
'OpenSSL::PKCS5' =>
|
136
|
+
'OpenSSL::PKey::PKey' => TargetMethods.new(:sign, OPENSSL_PACKAGES.(%w[crypto.pkey])),
|
137
|
+
'OpenSSL::X509::Request' => TargetMethods.new(%i[sign verify], OPENSSL_PACKAGES.(%w[crypto.x509])),
|
138
|
+
'OpenSSL::PKCS5' => TargetMethods.new(%i[pbkdf2_hmac_sha1 pbkdf2_hmac], OPENSSL_PACKAGES.(%w[crypto.pkcs5])),
|
109
139
|
'OpenSSL::Cipher' => [
|
110
|
-
|
111
|
-
|
140
|
+
TargetMethods.new(%i[encrypt], OPENSSL_PACKAGES.(%w[crypto.encrypt])),
|
141
|
+
TargetMethods.new(%i[decrypt], OPENSSL_PACKAGES.(%w[crypto.decrypt]))
|
112
142
|
],
|
113
143
|
'ActiveSupport::Callbacks::CallbackSequence' => [
|
114
|
-
|
115
|
-
|
144
|
+
TargetMethods.new(:invoke_before, Package.build_from_gem('activesupport', force: true, package_name: 'active_support', labels: %w[mvc.before_action])),
|
145
|
+
TargetMethods.new(:invoke_after, Package.build_from_gem('activesupport', force: true, package_name: 'active_support', labels: %w[mvc.after_action])),
|
116
146
|
],
|
117
|
-
'
|
118
|
-
'
|
147
|
+
'ActiveSupport::SecurityUtils' => TargetMethods.new(:secure_compare, Package.build_from_gem('activesupport', force: true, package_name: 'active_support/security_utils', labels: %w[crypto.secure_compare])),
|
148
|
+
'OpenSSL::X509::Certificate' => TargetMethods.new(:sign, OPENSSL_PACKAGES.(%w[crypto.x509])),
|
149
|
+
'Net::HTTP' => TargetMethods.new(:request, Package.build_from_path('net/http', package_name: 'net/http', labels: %w[protocol.http]).tap do |package|
|
119
150
|
package.handler_class = AppMap::Handler::NetHTTP
|
120
151
|
end),
|
121
|
-
'Net::SMTP' =>
|
122
|
-
'Net::POP3' =>
|
123
|
-
|
124
|
-
'
|
125
|
-
'
|
126
|
-
'
|
127
|
-
'JSON::Ext::
|
152
|
+
'Net::SMTP' => TargetMethods.new(:send, Package.build_from_path('net/smtp', package_name: 'net/smtp', labels: %w[protocol.email.smtp])),
|
153
|
+
'Net::POP3' => TargetMethods.new(:mails, Package.build_from_path('net/pop3', package_name: 'net/pop', labels: %w[protocol.email.pop])),
|
154
|
+
# This is happening: Method send_command not found on Net::IMAP
|
155
|
+
# 'Net::IMAP' => TargetMethods.new(:send_command, Package.build_from_path('net/imap', package_name: 'net/imap', labels: %w[protocol.email.imap])),
|
156
|
+
# 'Marshal' => TargetMethods.new(%i[dump load], Package.build_from_path('marshal', labels: %w[format.marshal])),
|
157
|
+
'Psych' => TargetMethods.new(%i[dump dump_stream load load_stream parse parse_stream], Package.build_from_path('yaml', package_name: 'psych', labels: %w[format.yaml])),
|
158
|
+
'JSON::Ext::Parser' => TargetMethods.new(:parse, Package.build_from_path('json', package_name: 'json', labels: %w[format.json])),
|
159
|
+
'JSON::Ext::Generator::State' => TargetMethods.new(:generate, Package.build_from_path('json', package_name: 'json', labels: %w[format.json])),
|
128
160
|
}.freeze
|
129
161
|
|
130
|
-
attr_reader :name, :packages, :exclude, :builtin_methods
|
162
|
+
attr_reader :name, :packages, :exclude, :hooked_methods, :builtin_methods
|
131
163
|
|
132
164
|
def initialize(name, packages, exclude: [], functions: [])
|
133
165
|
@name = name
|
134
166
|
@packages = packages
|
167
|
+
@hook_paths = packages.map(&:path)
|
135
168
|
@exclude = exclude
|
136
169
|
@builtin_methods = BUILTIN_METHODS
|
137
170
|
@functions = functions
|
@@ -140,7 +173,13 @@ module AppMap
|
|
140
173
|
package_options = {}
|
141
174
|
package_options[:labels] = func.labels if func.labels
|
142
175
|
@hooked_methods[func.cls] ||= []
|
143
|
-
@hooked_methods[func.cls] <<
|
176
|
+
@hooked_methods[func.cls] << TargetMethods.new(func.function_names, Package.build_from_path(func.package, package_options))
|
177
|
+
end
|
178
|
+
|
179
|
+
@hooked_methods.each_value do |hooks|
|
180
|
+
Array(hooks).each do |hook|
|
181
|
+
@hook_paths << hook.package.path if hook.package
|
182
|
+
end
|
144
183
|
end
|
145
184
|
end
|
146
185
|
|
@@ -191,57 +230,54 @@ module AppMap
|
|
191
230
|
}
|
192
231
|
end
|
193
232
|
|
194
|
-
#
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
end
|
199
|
-
|
200
|
-
def package_hooked_by_class(method)
|
201
|
-
defined_class, _, method_name = ::AppMap::Hook.qualify_method_name(method)
|
202
|
-
return find_package(defined_class, method_name)
|
233
|
+
# Determines if methods defined in a file path should possibly be hooked.
|
234
|
+
def path_enabled?(path)
|
235
|
+
path = AppMap::Util.normalize_path(path)
|
236
|
+
@hook_paths.find { |hook_path| path.index(hook_path) == 0 }
|
203
237
|
end
|
204
238
|
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
239
|
+
# Looks up a class and method in the config, to find the matching Package configuration.
|
240
|
+
# This class is only used after +path_enabled?+ has returned `true`.
|
241
|
+
LookupPackage = Struct.new(:config, :cls, :method) do
|
242
|
+
def package
|
243
|
+
# Global "excludes" configuration can be used to ignore any class/method.
|
244
|
+
return if config.never_hook?(cls, method)
|
209
245
|
|
210
|
-
|
211
|
-
packages.select { |pkg| pkg.path }.find do |pkg|
|
212
|
-
(location_file.index(pkg.path) == 0) &&
|
213
|
-
!pkg.exclude.find { |p| location_file.index(p) }
|
246
|
+
package_for_code_object || package_for_location
|
214
247
|
end
|
215
|
-
end
|
216
|
-
|
217
|
-
def never_hook?(method)
|
218
|
-
defined_class, separator, method_name = ::AppMap::Hook.qualify_method_name(method)
|
219
|
-
return true if exclude.member?(defined_class) || exclude.member?([ defined_class, separator, method_name ].join)
|
220
|
-
end
|
221
248
|
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
249
|
+
# Hook a method which is specified by class and method name.
|
250
|
+
def package_for_code_object
|
251
|
+
Array(config.hooked_methods[cls.name])
|
252
|
+
.compact
|
253
|
+
.find { |hook| hook.include_method?(method.name) }
|
254
|
+
&.package
|
255
|
+
end
|
226
256
|
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
257
|
+
# Hook a method which is specified by code location (i.e. path).
|
258
|
+
def package_for_location
|
259
|
+
location = method.source_location
|
260
|
+
location_file, = location
|
261
|
+
return unless location_file
|
262
|
+
|
263
|
+
location_file = AppMap::Util.normalize_path(location_file)
|
264
|
+
config
|
265
|
+
.packages
|
266
|
+
.select { |pkg| pkg.path }
|
267
|
+
.find do |pkg|
|
268
|
+
(location_file.index(pkg.path) == 0) &&
|
269
|
+
!pkg.exclude.find { |p| location_file.index(p) }
|
270
|
+
end
|
271
|
+
end
|
231
272
|
end
|
232
273
|
|
233
|
-
def
|
234
|
-
|
235
|
-
return nil unless hooks
|
236
|
-
|
237
|
-
hook = Array(hooks).find do |hook|
|
238
|
-
Array(hook.method_names).include?(method_name)
|
239
|
-
end
|
240
|
-
hook ? hook.package : nil
|
274
|
+
def lookup_package(cls, method)
|
275
|
+
LookupPackage.new(self, cls, method).package
|
241
276
|
end
|
242
277
|
|
243
|
-
def
|
244
|
-
|
278
|
+
def never_hook?(cls, method)
|
279
|
+
_, separator, = ::AppMap::Hook.qualify_method_name(method)
|
280
|
+
return true if exclude.member?(cls.name) || exclude.member?([ cls.name, separator, method.name ].join)
|
245
281
|
end
|
246
282
|
end
|
247
283
|
end
|
data/lib/appmap/hook.rb
CHANGED
@@ -5,6 +5,7 @@ require 'English'
|
|
5
5
|
module AppMap
|
6
6
|
class Hook
|
7
7
|
LOG = (ENV['APPMAP_DEBUG'] == 'true' || ENV['DEBUG'] == 'true')
|
8
|
+
LOG_HOOK = (ENV['DEBUG_HOOK'] == 'true')
|
8
9
|
|
9
10
|
OBJECT_INSTANCE_METHODS = %i[! != !~ <=> == === =~ __id__ __send__ class clone define_singleton_method display dup enum_for eql? equal? extend freeze frozen? hash inspect instance_eval instance_exec instance_of? instance_variable_defined? instance_variable_get instance_variable_set instance_variables is_a? itself kind_of? method methods nil? object_id private_methods protected_methods public_method public_methods public_send remove_instance_variable respond_to? send singleton_class singleton_method singleton_methods taint tainted? tap then to_enum to_s to_h to_a trust untaint untrust untrusted? yield_self].freeze
|
10
11
|
OBJECT_STATIC_METHODS = %i[! != !~ < <= <=> == === =~ > >= __id__ __send__ alias_method allocate ancestors attr attr_accessor attr_reader attr_writer autoload autoload? class class_eval class_exec class_variable_defined? class_variable_get class_variable_set class_variables clone const_defined? const_get const_missing const_set constants define_method define_singleton_method deprecate_constant display dup enum_for eql? equal? extend freeze frozen? hash include include? included_modules inspect instance_eval instance_exec instance_method instance_methods instance_of? instance_variable_defined? instance_variable_get instance_variable_set instance_variables is_a? itself kind_of? method method_defined? methods module_eval module_exec name new nil? object_id prepend private_class_method private_constant private_instance_methods private_method_defined? private_methods protected_instance_methods protected_method_defined? protected_methods public_class_method public_constant public_instance_method public_instance_methods public_method public_method_defined? public_methods public_send remove_class_variable remove_instance_variable remove_method respond_to? send singleton_class singleton_class? singleton_method singleton_methods superclass taint tainted? tap then to_enum to_s trust undef_method untaint untrust untrusted? yield_self].freeze
|
@@ -35,73 +36,21 @@ module AppMap
|
|
35
36
|
|
36
37
|
def initialize(config)
|
37
38
|
@config = config
|
39
|
+
@trace_locations = []
|
40
|
+
# Paths that are known to be non-tracing
|
41
|
+
@notrace_paths = Set.new
|
38
42
|
end
|
39
43
|
|
40
44
|
# Observe class loading and hook all methods which match the config.
|
41
|
-
def enable
|
45
|
+
def enable(&block)
|
42
46
|
require 'appmap/hook/method'
|
43
47
|
|
44
48
|
hook_builtins
|
45
49
|
|
46
|
-
|
47
|
-
|
50
|
+
@trace_begin = TracePoint.new(:class, &method(:trace_class))
|
51
|
+
@trace_end = TracePoint.new(:end, &method(:trace_end))
|
48
52
|
|
49
|
-
|
50
|
-
# NoMethodError: private method `singleton_class' called for Rack::MiniProfiler:Class
|
51
|
-
class_methods = begin
|
52
|
-
if cls.respond_to?(:singleton_class)
|
53
|
-
cls.singleton_class.public_instance_methods(false) - instance_methods - OBJECT_STATIC_METHODS
|
54
|
-
else
|
55
|
-
[]
|
56
|
-
end
|
57
|
-
rescue NameError
|
58
|
-
[]
|
59
|
-
end
|
60
|
-
|
61
|
-
hook = lambda do |hook_cls|
|
62
|
-
lambda do |method_id|
|
63
|
-
# Don't try and trace the AppMap methods or there will be
|
64
|
-
# a stack overflow in the defined hook method.
|
65
|
-
return if (hook_cls&.name || '').split('::')[0] == AppMap.name
|
66
|
-
|
67
|
-
method = begin
|
68
|
-
hook_cls.public_instance_method(method_id)
|
69
|
-
rescue NameError
|
70
|
-
warn "AppMap: Method #{hook_cls} #{method.name} is not accessible" if LOG
|
71
|
-
return
|
72
|
-
end
|
73
|
-
|
74
|
-
warn "AppMap: Examining #{hook_cls} #{method.name}" if LOG
|
75
|
-
|
76
|
-
disasm = RubyVM::InstructionSequence.disasm(method)
|
77
|
-
# Skip methods that have no instruction sequence, as they are obviously trivial.
|
78
|
-
next unless disasm
|
79
|
-
|
80
|
-
next if config.never_hook?(method)
|
81
|
-
|
82
|
-
next unless \
|
83
|
-
config.always_hook?(hook_cls, method.name) ||
|
84
|
-
config.included_by_location?(method)
|
85
|
-
|
86
|
-
package = config.package_for_method(method)
|
87
|
-
|
88
|
-
hook_method = Hook::Method.new(package, hook_cls, method)
|
89
|
-
|
90
|
-
hook_method.activate
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
instance_methods.each(&hook.(cls))
|
95
|
-
# NoMethodError: private method `singleton_class' called for Rack::MiniProfiler:Class
|
96
|
-
begin
|
97
|
-
class_methods.each(&hook.(cls.singleton_class)) if cls.respond_to?(:singleton_class)
|
98
|
-
rescue NameError
|
99
|
-
# NameError:
|
100
|
-
# uninitialized constant Faraday::Connection
|
101
|
-
end
|
102
|
-
end
|
103
|
-
|
104
|
-
tp.enable(&block)
|
53
|
+
@trace_begin.enable(&block)
|
105
54
|
end
|
106
55
|
|
107
56
|
# hook_builtins builds hooks for code that is built in to the Ruby standard library.
|
@@ -120,25 +69,112 @@ module AppMap
|
|
120
69
|
require hook.package.package_name if hook.package.package_name
|
121
70
|
Array(hook.method_names).each do |method_name|
|
122
71
|
method_name = method_name.to_sym
|
72
|
+
base_cls = class_from_string.(class_name)
|
123
73
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
cls.instance_method(method_name)
|
128
|
-
rescue NameError
|
129
|
-
cls.method(method_name) rescue nil
|
130
|
-
end
|
131
|
-
|
132
|
-
next if config.never_hook?(method)
|
74
|
+
hook_method = lambda do |entry|
|
75
|
+
cls, method = entry
|
76
|
+
return false if config.never_hook?(cls, method)
|
133
77
|
|
134
|
-
if method
|
135
78
|
Hook::Method.new(hook.package, cls, method).activate
|
79
|
+
end
|
80
|
+
|
81
|
+
methods = []
|
82
|
+
methods << [ base_cls, base_cls.public_instance_method(method_name) ] rescue nil
|
83
|
+
if base_cls.respond_to?(:singleton_class)
|
84
|
+
methods << [ base_cls.singleton_class, base_cls.singleton_class.public_instance_method(method_name) ] rescue nil
|
85
|
+
end
|
86
|
+
methods.compact!
|
87
|
+
if methods.empty?
|
88
|
+
warn "Method #{method_name} not found on #{base_cls.name}"
|
136
89
|
else
|
137
|
-
|
90
|
+
methods.each(&hook_method)
|
138
91
|
end
|
139
92
|
end
|
140
93
|
end
|
141
94
|
end
|
142
95
|
end
|
96
|
+
|
97
|
+
protected
|
98
|
+
|
99
|
+
def trace_class(trace_point)
|
100
|
+
path = trace_point.path
|
101
|
+
|
102
|
+
return if @notrace_paths.member?(path)
|
103
|
+
|
104
|
+
if config.path_enabled?(path)
|
105
|
+
location = trace_location(trace_point)
|
106
|
+
warn "Entering hook-enabled location #{location}" if Hook::LOG || Hook::LOG_HOOK
|
107
|
+
@trace_locations << location
|
108
|
+
unless @trace_end.enabled?
|
109
|
+
warn "Enabling hooking" if Hook::LOG || Hook::LOG_HOOK
|
110
|
+
@trace_end.enable
|
111
|
+
end
|
112
|
+
else
|
113
|
+
@notrace_paths << path
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def trace_location(trace_point)
|
118
|
+
[ trace_point.path, trace_point.lineno ].join(':')
|
119
|
+
end
|
120
|
+
|
121
|
+
def trace_end(trace_point)
|
122
|
+
cls = trace_point.self
|
123
|
+
|
124
|
+
instance_methods = cls.public_instance_methods(false) - OBJECT_INSTANCE_METHODS
|
125
|
+
# NoMethodError: private method `singleton_class' called for Rack::MiniProfiler:Class
|
126
|
+
class_methods = begin
|
127
|
+
if cls.respond_to?(:singleton_class)
|
128
|
+
cls.singleton_class.public_instance_methods(false) - instance_methods - OBJECT_STATIC_METHODS
|
129
|
+
else
|
130
|
+
[]
|
131
|
+
end
|
132
|
+
rescue NameError
|
133
|
+
[]
|
134
|
+
end
|
135
|
+
|
136
|
+
hook = lambda do |hook_cls|
|
137
|
+
lambda do |method_id|
|
138
|
+
# Don't try and trace the AppMap methods or there will be
|
139
|
+
# a stack overflow in the defined hook method.
|
140
|
+
next if %w[Marshal AppMap ActiveSupport].member?((hook_cls&.name || '').split('::')[0])
|
141
|
+
|
142
|
+
method = begin
|
143
|
+
hook_cls.public_instance_method(method_id)
|
144
|
+
rescue NameError
|
145
|
+
warn "AppMap: Method #{hook_cls} #{method.name} is not accessible" if LOG
|
146
|
+
next
|
147
|
+
end
|
148
|
+
|
149
|
+
warn "AppMap: Examining #{hook_cls} #{method.name}" if LOG
|
150
|
+
|
151
|
+
disasm = RubyVM::InstructionSequence.disasm(method)
|
152
|
+
# Skip methods that have no instruction sequence, as they are obviously trivial.
|
153
|
+
next unless disasm
|
154
|
+
|
155
|
+
package = config.lookup_package(hook_cls, method)
|
156
|
+
next unless package
|
157
|
+
|
158
|
+
Hook::Method.new(package, hook_cls, method).activate
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
instance_methods.each(&hook.(cls))
|
163
|
+
begin
|
164
|
+
# NoMethodError: private method `singleton_class' called for Rack::MiniProfiler:Class
|
165
|
+
class_methods.each(&hook.(cls.singleton_class)) if cls.respond_to?(:singleton_class)
|
166
|
+
rescue NameError
|
167
|
+
# NameError:
|
168
|
+
# uninitialized constant Faraday::Connection
|
169
|
+
warn "NameError in #{__FILE__}: #{$!.message}"
|
170
|
+
end
|
171
|
+
|
172
|
+
location = @trace_locations.pop
|
173
|
+
warn "Leaving hook-enabled location #{location}" if Hook::LOG || Hook::LOG_HOOK
|
174
|
+
if @trace_locations.empty?
|
175
|
+
warn "Disabling hooking" if Hook::LOG || Hook::LOG_HOOK
|
176
|
+
@trace_end.disable
|
177
|
+
end
|
178
|
+
end
|
143
179
|
end
|
144
180
|
end
|
data/lib/appmap/util.rb
CHANGED
@@ -93,6 +93,14 @@ module AppMap
|
|
93
93
|
matching_headers.blank? ? nil : matching_headers
|
94
94
|
end
|
95
95
|
|
96
|
+
def normalize_path(path)
|
97
|
+
if path.index(Dir.pwd) == 0
|
98
|
+
path[Dir.pwd.length + 1..-1]
|
99
|
+
else
|
100
|
+
path
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
96
104
|
# Atomically writes AppMap data to +filename+.
|
97
105
|
def write_appmap(filename, appmap)
|
98
106
|
require 'fileutils'
|
data/lib/appmap/version.rb
CHANGED
@@ -177,7 +177,7 @@ describe 'Rails' do
|
|
177
177
|
)
|
178
178
|
|
179
179
|
expect(appmap['classMap']).to include hash_including(
|
180
|
-
'name' => '
|
180
|
+
'name' => 'actionview',
|
181
181
|
'children' => include(hash_including(
|
182
182
|
'name' => 'ActionView',
|
183
183
|
'children' => include(hash_including(
|
data/spec/hook_spec.rb
CHANGED
@@ -60,8 +60,8 @@ describe 'AppMap class Hooking', docker: false do
|
|
60
60
|
config = AppMap::Config.new('hook_spec', [ package ], exclude: %w[ExcludeTest])
|
61
61
|
AppMap.configuration = config
|
62
62
|
|
63
|
-
expect(config.never_hook?(ExcludeTest.new.method(:instance_method))).to be_truthy
|
64
|
-
expect(config.never_hook?(ExcludeTest.method(:cls_method))).to be_truthy
|
63
|
+
expect(config.never_hook?(ExcludeTest, ExcludeTest.new.method(:instance_method))).to be_truthy
|
64
|
+
expect(config.never_hook?(ExcludeTest, ExcludeTest.method(:cls_method))).to be_truthy
|
65
65
|
end
|
66
66
|
|
67
67
|
it "handles an instance method named 'call' without issues" do
|
@@ -163,7 +163,9 @@ describe 'AppMap class Hooking', docker: false do
|
|
163
163
|
method = hook_cls.instance_method(:say_default)
|
164
164
|
|
165
165
|
require 'appmap/hook/method'
|
166
|
-
|
166
|
+
package = config.lookup_package(hook_cls, method)
|
167
|
+
expect(package).to be
|
168
|
+
hook_method = AppMap::Hook::Method.new(package, hook_cls, method)
|
167
169
|
hook_method.activate
|
168
170
|
|
169
171
|
tracer = AppMap.tracing.trace
|
@@ -861,7 +863,9 @@ describe 'AppMap class Hooking', docker: false do
|
|
861
863
|
_, _, events = test_hook_behavior 'spec/fixtures/hook/compare.rb', nil do
|
862
864
|
expect(Compare.compare('string', 'string')).to be_truthy
|
863
865
|
end
|
866
|
+
|
864
867
|
secure_compare_event = YAML.load(events).find { |evt| evt[:defined_class] == 'ActiveSupport::SecurityUtils' }
|
868
|
+
expect(secure_compare_event).to be_truthy
|
865
869
|
secure_compare_event.delete(:lineno)
|
866
870
|
secure_compare_event.delete(:path)
|
867
871
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: appmap
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.45.
|
4
|
+
version: 0.45.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kevin Gilpin
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-05-
|
11
|
+
date: 2021-05-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|