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