appmap 0.45.0 → 0.45.1
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 +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
|
[](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
|