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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0db499c7da4baea3f29fb56214a95e18047a07fc0c132ba559f7f6eb7ef9033a
4
- data.tar.gz: 3aabf3bf1a31c39d6844ccf17807a4b7e206783bdf8e373008d3db0d04a854d7
3
+ metadata.gz: b91b79723565f45d9d59ce341a9944a3a4e1c853bc77c1b176dc7b26ede9df22
4
+ data.tar.gz: f9be88ae7e83b801f66ada4087a06923ad9fa4d10e498f1c8b07fe0e301552d7
5
5
  SHA512:
6
- metadata.gz: a38e51f5c932d9a04b566c2664104e08a56af80fd3277c039603eb719336599bc9764781e1d0835c6c7b702e4e0465587fd4537e292df325c0d9efe4a90c1493
7
- data.tar.gz: 168e2b8c1605cf7b4f8c4d6a5f373b5add4ce1804c4c494f3cc4e3b29da5a08f4e121889bca36e06e1e29aa4c9943c16b76f7868e9331c3a7e5b33af0817f1a0
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
- def build_from_gem(gem, shallow: true, package_name: nil, exclude: [], labels: [])
26
- if %w[method_source activesupport].member?(gem)
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
- Package.new(gem_path(gem), gem, package_name, exclude, labels, shallow)
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] or raise "Gem #{gem.inspect} not found"
39
- gemspec.gem_dir
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 Hook
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
- 'ActiveSupport::SecurityUtils' => Hook.new(:secure_compare, Package.build_from_path('active_support', labels: %w[crypto.secure_compare])),
93
- 'ActionView::Renderer' => Hook.new(:render, Package.build_from_path('action_view', labels: %w[mvc.view])),
94
- 'ActionDispatch::Request::Session' => Hook.new(%i[destroy [] dig values []= clear update delete fetch merge], Package.build_from_path('action_pack', labels: %w[http.session])),
95
- 'ActionDispatch::Cookies::CookieJar' => Hook.new(%i[[]= clear update delete recycle], Package.build_from_path('action_pack', labels: %w[http.cookie])),
96
- 'ActionDispatch::Cookies::EncryptedCookieJar' => Hook.new(%i[[]=], Package.build_from_path('action_pack', labels: %w[http.cookie crypto.encrypt])),
97
- 'CanCan::ControllerAdditions' => Hook.new(%i[authorize! can? cannot?], Package.build_from_path('cancancan', labels: %w[security.authorization])),
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
- Hook.new(%i[process_action send_file send_data redirect_to], Package.build_from_path('action_view', labels: %w[mvc.controller])),
101
- Hook.new(%i[render], Package.build_from_path('action_view', labels: %w[mvc.view])),
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' => Hook.new(:sign, OPENSSL_PACKAGES.(%w[crypto.pkey])),
107
- 'OpenSSL::X509::Request' => Hook.new(%i[sign verify], OPENSSL_PACKAGES.(%w[crypto.x509])),
108
- 'OpenSSL::PKCS5' => Hook.new(%i[pbkdf2_hmac_sha1 pbkdf2_hmac], OPENSSL_PACKAGES.(%w[crypto.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
- Hook.new(%i[encrypt], OPENSSL_PACKAGES.(%w[crypto.encrypt])),
111
- Hook.new(%i[decrypt], OPENSSL_PACKAGES.(%w[crypto.decrypt]))
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
- Hook.new(:invoke_before, Package.build_from_path('active_support', package_name: 'active_support', labels: %w[mvc.before_action])),
115
- Hook.new(:invoke_after, Package.build_from_path('active_support', package_name: 'active_support', labels: %w[mvc.after_action])),
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
- 'OpenSSL::X509::Certificate' => Hook.new(:sign, OPENSSL_PACKAGES.(%w[crypto.x509])),
118
- 'Net::HTTP' => Hook.new(:request, Package.build_from_path('net/http', package_name: 'net/http', labels: %w[protocol.http]).tap do |package|
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' => Hook.new(:send, Package.build_from_path('net/smtp', package_name: 'net/smtp', labels: %w[protocol.email.smtp])),
122
- 'Net::POP3' => Hook.new(:mails, Package.build_from_path('net/pop3', package_name: 'net/pop', labels: %w[protocol.email.pop])),
123
- 'Net::IMAP' => Hook.new(:send_command, Package.build_from_path('net/imap', package_name: 'net/imap', labels: %w[protocol.email.imap])),
124
- 'Marshal' => Hook.new(%i[dump load], Package.build_from_path('marshal', labels: %w[format.marshal])),
125
- 'Psych' => Hook.new(%i[dump dump_stream load load_stream parse parse_stream], Package.build_from_path('yaml', package_name: 'psych', labels: %w[format.yaml])),
126
- 'JSON::Ext::Parser' => Hook.new(:parse, Package.build_from_path('json', package_name: 'json', labels: %w[format.json])),
127
- 'JSON::Ext::Generator::State' => Hook.new(:generate, Package.build_from_path('json', package_name: 'json', labels: %w[format.json])),
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] << Hook.new(func.function_names, Package.build_from_path(func.package, package_options))
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
- # package_for_method finds the Package, if any, which configures the hook
195
- # for a method.
196
- def package_for_method(method)
197
- package_hooked_by_class(method) || package_hooked_by_source_location(method)
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
- def package_hooked_by_source_location(method)
206
- location = method.source_location
207
- location_file, = location
208
- return unless location_file
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
- location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
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
- # always_hook? indicates a method that should always be hooked.
223
- def always_hook?(defined_class, method_name)
224
- !!find_package(defined_class, method_name)
225
- end
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
- # included_by_location? indicates a method whose source location matches a method definition that has been
228
- # configured for inclusion.
229
- def included_by_location?(method)
230
- !!package_for_method(method)
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 find_package(defined_class, method_name)
234
- hooks = find_hooks(defined_class)
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 find_hooks(defined_class)
244
- Array(@hooked_methods[defined_class] || @builtin_methods[defined_class])
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 &block
45
+ def enable(&block)
42
46
  require 'appmap/hook/method'
43
47
 
44
48
  hook_builtins
45
49
 
46
- tp = TracePoint.new(:end) do |trace_point|
47
- cls = trace_point.self
50
+ @trace_begin = TracePoint.new(:class, &method(:trace_class))
51
+ @trace_end = TracePoint.new(:end, &method(:trace_end))
48
52
 
49
- instance_methods = cls.public_instance_methods(false) - OBJECT_INSTANCE_METHODS
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
- cls = class_from_string.(class_name)
125
- method = \
126
- begin
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
- warn "Method #{method_name} not found on #{cls.name}"
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'
@@ -3,7 +3,7 @@
3
3
  module AppMap
4
4
  URL = 'https://github.com/applandinc/appmap-ruby'
5
5
 
6
- VERSION = '0.45.0'
6
+ VERSION = '0.45.1'
7
7
 
8
8
  APPMAP_FORMAT_VERSION = '1.5.0'
9
9
  end
@@ -177,7 +177,7 @@ describe 'Rails' do
177
177
  )
178
178
 
179
179
  expect(appmap['classMap']).to include hash_including(
180
- 'name' => 'action_view',
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
- hook_method = AppMap::Hook::Method.new(config.package_for_method(method), hook_cls, method)
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.0
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-03 00:00:00.000000000 Z
11
+ date: 2021-05-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport