appmap 0.26.0 → 0.31.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -2
  3. data/CHANGELOG.md +38 -0
  4. data/README.md +144 -31
  5. data/Rakefile +1 -1
  6. data/exe/appmap +3 -1
  7. data/lib/appmap.rb +55 -35
  8. data/lib/appmap/algorithm/stats.rb +2 -1
  9. data/lib/appmap/class_map.rb +16 -24
  10. data/lib/appmap/command/record.rb +2 -61
  11. data/lib/appmap/config.rb +91 -0
  12. data/lib/appmap/cucumber.rb +89 -0
  13. data/lib/appmap/event.rb +6 -6
  14. data/lib/appmap/hook.rb +94 -116
  15. data/lib/appmap/metadata.rb +62 -0
  16. data/lib/appmap/middleware/remote_recording.rb +2 -6
  17. data/lib/appmap/minitest.rb +141 -0
  18. data/lib/appmap/rails/action_handler.rb +2 -2
  19. data/lib/appmap/rails/sql_handler.rb +2 -2
  20. data/lib/appmap/railtie.rb +2 -2
  21. data/lib/appmap/record.rb +27 -0
  22. data/lib/appmap/rspec.rb +20 -38
  23. data/lib/appmap/trace.rb +19 -11
  24. data/lib/appmap/util.rb +40 -0
  25. data/lib/appmap/version.rb +1 -1
  26. data/package-lock.json +3 -3
  27. data/spec/abstract_controller4_base_spec.rb +1 -1
  28. data/spec/abstract_controller_base_spec.rb +1 -1
  29. data/spec/config_spec.rb +3 -3
  30. data/spec/fixtures/hook/compare.rb +7 -0
  31. data/spec/fixtures/hook/openssl_sign.rb +87 -0
  32. data/spec/fixtures/hook/singleton_method.rb +54 -0
  33. data/spec/fixtures/rails_users_app/Gemfile +1 -0
  34. data/spec/fixtures/rails_users_app/features/api_users.feature +13 -0
  35. data/spec/fixtures/rails_users_app/features/support/env.rb +4 -0
  36. data/spec/fixtures/rails_users_app/features/support/hooks.rb +11 -0
  37. data/spec/fixtures/rails_users_app/features/support/steps.rb +18 -0
  38. data/spec/hook_spec.rb +243 -36
  39. data/spec/rails_spec_helper.rb +2 -0
  40. data/spec/rspec_feature_metadata_spec.rb +2 -0
  41. data/spec/spec_helper.rb +4 -0
  42. data/spec/util_spec.rb +21 -0
  43. data/test/cli_test.rb +2 -2
  44. data/test/cucumber_test.rb +72 -0
  45. data/test/fixtures/cucumber4_recorder/Gemfile +5 -0
  46. data/test/fixtures/cucumber4_recorder/appmap.yml +3 -0
  47. data/test/fixtures/cucumber4_recorder/features/say_hello.feature +5 -0
  48. data/test/fixtures/cucumber4_recorder/features/support/env.rb +5 -0
  49. data/test/fixtures/cucumber4_recorder/features/support/hooks.rb +11 -0
  50. data/test/fixtures/cucumber4_recorder/features/support/steps.rb +9 -0
  51. data/test/fixtures/cucumber4_recorder/lib/hello.rb +7 -0
  52. data/test/fixtures/cucumber_recorder/Gemfile +5 -0
  53. data/test/fixtures/cucumber_recorder/appmap.yml +3 -0
  54. data/test/fixtures/cucumber_recorder/features/say_hello.feature +5 -0
  55. data/test/fixtures/cucumber_recorder/features/support/env.rb +5 -0
  56. data/test/fixtures/cucumber_recorder/features/support/hooks.rb +11 -0
  57. data/test/fixtures/cucumber_recorder/features/support/steps.rb +9 -0
  58. data/test/fixtures/cucumber_recorder/lib/hello.rb +7 -0
  59. data/test/fixtures/minitest_recorder/Gemfile +5 -0
  60. data/test/fixtures/minitest_recorder/appmap.yml +3 -0
  61. data/test/fixtures/minitest_recorder/lib/hello.rb +5 -0
  62. data/test/fixtures/minitest_recorder/test/hello_test.rb +12 -0
  63. data/test/fixtures/process_recorder/appmap.yml +3 -0
  64. data/test/fixtures/process_recorder/hello.rb +9 -0
  65. data/test/fixtures/rspec_recorder/Gemfile +1 -1
  66. data/test/fixtures/rspec_recorder/spec/decorated_hello_spec.rb +12 -0
  67. data/test/minitest_test.rb +38 -0
  68. data/test/record_process_test.rb +35 -0
  69. data/test/rspec_test.rb +5 -0
  70. metadata +39 -3
  71. data/spec/fixtures/hook/class_method.rb +0 -17
@@ -74,10 +74,11 @@ module AppMap
74
74
 
75
75
  class_name_func = ->(event) { event['defined_class'] }
76
76
  full_name_func = lambda do |event|
77
+ call = event['event'] == 'call'
77
78
  class_name = event['defined_class']
78
79
  static = event['static']
79
80
  function_name = event['method_id']
80
- [ class_name, static ? '.' : '#', function_name ].join if class_name && !static.nil? && function_name
81
+ [ class_name, static ? '.' : '#', function_name ].join if call && class_name && !static.nil? && function_name
81
82
  end
82
83
 
83
84
  class_frequency = frequency_calc.call(class_name_func)
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_support/core_ext'
4
-
5
3
  module AppMap
6
4
  class ClassMap
7
5
  module HasChildren
@@ -50,7 +48,7 @@ module AppMap
50
48
  end
51
49
  end
52
50
  Function = Struct.new(:name) do
53
- attr_accessor :static, :location
51
+ attr_accessor :static, :location, :labels
54
52
 
55
53
  def type
56
54
  'function'
@@ -61,8 +59,9 @@ module AppMap
61
59
  name: name,
62
60
  type: type,
63
61
  location: location,
64
- static: static
65
- }
62
+ static: static,
63
+ labels: labels
64
+ }.delete_if {|k,v| v.nil?}
66
65
  end
67
66
  end
68
67
  end
@@ -71,35 +70,25 @@ module AppMap
71
70
  def build_from_methods(config, methods)
72
71
  root = Types::Root.new
73
72
  methods.each do |method|
74
- package = package_for_method(config.packages, method)
75
- add_function root, package.path, method
73
+ package = config.package_for_method(method) \
74
+ or raise "No package found for method #{method}"
75
+ add_function root, package, method
76
76
  end
77
77
  root.children.map(&:to_h)
78
78
  end
79
79
 
80
80
  protected
81
81
 
82
- def package_for_method(packages, method)
83
- location = method.method.source_location
84
- location_file, = location
85
- location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
86
-
87
- packages.find do |pkg|
88
- (location_file.index(pkg.path) == 0) &&
89
- !pkg.exclude.find { |p| location_file.index(p) }
90
- end or raise "No package found for method #{method}"
91
- end
92
-
93
- def add_function(root, package_name, method)
94
- location = method.method.source_location
82
+ def add_function(root, package, method)
83
+ location = method.source_location
95
84
  location_file, lineno = location
96
85
  location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
97
86
 
98
- static = method.method.owner.singleton_class?
87
+ static = method.static
99
88
 
100
89
  object_infos = [
101
90
  {
102
- name: package_name,
91
+ name: package.path,
103
92
  type: 'package'
104
93
  }
105
94
  ]
@@ -109,12 +98,15 @@ module AppMap
109
98
  type: 'class'
110
99
  }
111
100
  end
112
- object_infos << {
113
- name: method.method.name,
101
+ function_info = {
102
+ name: method.name,
114
103
  type: 'function',
115
104
  location: [ location_file, lineno ].join(':'),
116
105
  static: static
117
106
  }
107
+ function_info[:labels] = package.labels if package.labels
108
+ object_infos << function_info
109
+
118
110
  parent = root
119
111
  object_infos.each do |info|
120
112
  parent = find_or_create parent.children, info do
@@ -5,66 +5,7 @@ module AppMap
5
5
  RecordStruct = Struct.new(:config, :program)
6
6
 
7
7
  class Record < RecordStruct
8
- class << self
9
- # Builds a Hash of metadata which can be detected by inspecting the system.
10
- def detect_metadata
11
- {
12
- language: {
13
- name: 'ruby',
14
- engine: RUBY_ENGINE,
15
- version: RUBY_VERSION
16
- },
17
- client: {
18
- name: 'appmap',
19
- url: AppMap::URL,
20
- version: AppMap::VERSION
21
- }
22
- }.tap do |m|
23
- if defined?(::Rails)
24
- m[:frameworks] ||= []
25
- m[:frameworks] << {
26
- name: 'rails',
27
- version: ::Rails.version
28
- }
29
- end
30
- m[:git] = git_metadata if git_available
31
- end
32
- end
33
-
34
- protected
35
-
36
- def git_available
37
- @git_available = system('git status 2>&1 > /dev/null') if @git_available.nil?
38
- end
39
-
40
- def git_metadata
41
- git_repo = `git config --get remote.origin.url`.strip
42
- git_branch = `git rev-parse --abbrev-ref HEAD`.strip
43
- git_sha = `git rev-parse HEAD`.strip
44
- git_status = `git status -s`.split("\n").map(&:strip)
45
- git_last_annotated_tag = `git describe --abbrev=0 2>/dev/null`.strip
46
- git_last_annotated_tag = nil if git_last_annotated_tag.blank?
47
- git_last_tag = `git describe --abbrev=0 --tags 2>/dev/null`.strip
48
- git_last_tag = nil if git_last_tag.blank?
49
- git_commits_since_last_annotated_tag = `git describe`.strip =~ /-(\d+)-(\w+)$/[1] rescue 0 if git_last_annotated_tag
50
- git_commits_since_last_tag = `git describe --tags`.strip =~ /-(\d+)-(\w+)$/[1] rescue 0 if git_last_tag
51
-
52
- {
53
- repository: git_repo,
54
- branch: git_branch,
55
- commit: git_sha,
56
- status: git_status,
57
- git_last_annotated_tag: git_last_annotated_tag,
58
- git_last_tag: git_last_tag,
59
- git_commits_since_last_annotated_tag: git_commits_since_last_annotated_tag,
60
- git_commits_since_last_tag: git_commits_since_last_tag
61
- }
62
- end
63
- end
64
-
65
8
  def perform(&block)
66
- AppMap::Hook.hook(config)
67
-
68
9
  tracer = AppMap.tracing.trace
69
10
 
70
11
  events = []
@@ -85,8 +26,8 @@ module AppMap
85
26
  quit = true
86
27
  event_thread.join
87
28
  yield AppMap::APPMAP_FORMAT_VERSION,
88
- self.class.detect_metadata,
89
- AppMap.class_map(config, tracer.event_methods),
29
+ AppMap.detect_metadata,
30
+ AppMap.class_map(tracer.event_methods),
90
31
  events
91
32
  end
92
33
 
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppMap
4
+ Package = Struct.new(:path, :exclude, :labels) do
5
+ def initialize(path, exclude, labels = nil)
6
+ super
7
+ end
8
+
9
+ def to_h
10
+ {
11
+ path: path,
12
+ exclude: exclude.blank? ? nil : exclude,
13
+ labels: labels.blank? ? nil : labels
14
+ }.compact
15
+ end
16
+ end
17
+
18
+ class Config
19
+ # Methods that should always be hooked, with their containing
20
+ # package and labels that should be applied to them.
21
+ HOOKED_METHODS = {
22
+ 'ActiveSupport::SecurityUtils' => {
23
+ secure_compare: Package.new('active_support', nil, ['security'])
24
+ },
25
+ 'OpenSSL::X509::Certificate' => {
26
+ sign: Package.new('openssl', nil, ['security'])
27
+ }
28
+ }
29
+
30
+ attr_reader :name, :packages
31
+ def initialize(name, packages = [])
32
+ @name = name
33
+ @packages = packages
34
+ end
35
+
36
+ class << self
37
+ # Loads configuration data from a file, specified by the file name.
38
+ def load_from_file(config_file_name)
39
+ require 'yaml'
40
+ load YAML.safe_load(::File.read(config_file_name))
41
+ end
42
+
43
+ # Loads configuration from a Hash.
44
+ def load(config_data)
45
+ packages = (config_data['packages'] || []).map do |package|
46
+ Package.new(package['path'], package['exclude'] || [])
47
+ end
48
+ Config.new config_data['name'], packages
49
+ end
50
+ end
51
+
52
+ def to_h
53
+ {
54
+ name: name,
55
+ packages: packages.map(&:to_h)
56
+ }
57
+ end
58
+
59
+ def package_for_method(method)
60
+ location = method.source_location
61
+ location_file, = location
62
+ return unless location_file
63
+
64
+ defined_class,_,method_name = Hook.qualify_method_name(method)
65
+ hooked_method = find_hooked_method(defined_class, method_name)
66
+ return hooked_method if hooked_method
67
+
68
+ location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
69
+ packages.find do |pkg|
70
+ (location_file.index(pkg.path) == 0) &&
71
+ !pkg.exclude.find { |p| location_file.index(p) }
72
+ end
73
+ end
74
+
75
+ def included_by_location?(method)
76
+ !!package_for_method(method)
77
+ end
78
+
79
+ def always_hook?(defined_class, method_name)
80
+ !!find_hooked_method(defined_class, method_name)
81
+ end
82
+
83
+ def find_hooked_method(defined_class, method_name)
84
+ find_hooked_class(defined_class)[method_name]
85
+ end
86
+
87
+ def find_hooked_class(defined_class)
88
+ HOOKED_METHODS[defined_class] || {}
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'appmap/util'
4
+
5
+ module AppMap
6
+ module Cucumber
7
+ ScenarioAttributes = Struct.new(:name, :feature, :feature_group)
8
+
9
+ ProviderStruct = Struct.new(:scenario) do
10
+ def feature_group
11
+ # e.g. <Cucumber::Core::Ast::Location::Precise: cucumber/api/features/authenticate.feature:1>
12
+ feature_path.split('/').last.split('.')[0]
13
+ end
14
+ end
15
+
16
+ # ProviderBefore4 provides scenario name, feature name, and feature group name for Cucumber
17
+ # versions before 4.0.
18
+ class ProviderBefore4 < ProviderStruct
19
+ def attributes
20
+ ScenarioAttributes.new(scenario.name, scenario.feature.name, feature_group)
21
+ end
22
+
23
+ def feature_path
24
+ scenario.feature.location.to_s
25
+ end
26
+ end
27
+
28
+ # Provider4 provides scenario name, feature name, and feature group name for Cucumber
29
+ # versions 4.0 and later.
30
+ class Provider4 < ProviderStruct
31
+ def attributes
32
+ ScenarioAttributes.new(scenario.name, scenario.name.split(' ')[0..1].join(' '), feature_group)
33
+ end
34
+
35
+ def feature_path
36
+ scenario.location.file
37
+ end
38
+ end
39
+
40
+ class << self
41
+ def write_scenario(scenario, appmap)
42
+ appmap['metadata'] = update_metadata(scenario, appmap['metadata'])
43
+ scenario_filename = AppMap::Util.scenario_filename(appmap['metadata']['name'])
44
+
45
+ FileUtils.mkdir_p 'tmp/appmap/cucumber'
46
+ File.write(File.join('tmp/appmap/cucumber', scenario_filename), JSON.generate(appmap))
47
+ end
48
+
49
+ def enabled?
50
+ ENV['APPMAP'] == 'true'
51
+ end
52
+
53
+ protected
54
+
55
+ def cucumber_version
56
+ Gem.loaded_specs['cucumber']&.version&.to_s
57
+ end
58
+
59
+ def provider(scenario)
60
+ major, = cucumber_version.split('.').map(&:to_i)
61
+ if major < 4
62
+ ProviderBefore4
63
+ else
64
+ Provider4
65
+ end.new(scenario)
66
+ end
67
+
68
+ def update_metadata(scenario, base_metadata)
69
+ attributes = provider(scenario).attributes
70
+
71
+ base_metadata.tap do |m|
72
+ m['name'] = attributes.name
73
+ m['feature'] = attributes.feature
74
+ m['feature_group'] = attributes.feature_group
75
+ m['labels'] ||= []
76
+ m['labels'] += (scenario.tags&.map(&:name) || [])
77
+ m['frameworks'] ||= []
78
+ m['frameworks'] << {
79
+ 'name' => 'cucumber',
80
+ 'version' => Gem.loaded_specs['cucumber']&.version&.to_s
81
+ }
82
+ m['recorder'] = {
83
+ 'name' => 'cucumber'
84
+ }
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -15,15 +15,13 @@ module AppMap
15
15
  end
16
16
  end
17
17
 
18
- MethodEventStruct = Struct.new(:id, :event, :defined_class, :method_id, :path, :lineno, :static, :thread_id)
18
+ MethodEventStruct = Struct.new(:id, :event, :defined_class, :method_id, :path, :lineno, :thread_id)
19
19
 
20
20
  class MethodEvent < MethodEventStruct
21
21
  LIMIT = 100
22
22
 
23
23
  class << self
24
24
  def build_from_invocation(me, event_type, defined_class, method)
25
- singleton = method.owner.singleton_class?
26
-
27
25
  me.id = AppMap::Event.next_id_counter
28
26
  me.event = event_type
29
27
  me.defined_class = defined_class
@@ -32,7 +30,6 @@ module AppMap
32
30
  path = path[Dir.pwd.length + 1..-1] if path.index(Dir.pwd) == 0
33
31
  me.path = path
34
32
  me.lineno = method.source_location[1]
35
- me.static = singleton
36
33
  me.thread_id = Thread.current.object_id
37
34
  end
38
35
 
@@ -62,11 +59,10 @@ module AppMap
62
59
  end
63
60
  end
64
61
 
65
- alias static? static
66
62
  end
67
63
 
68
64
  class MethodCall < MethodEvent
69
- attr_accessor :parameters, :receiver
65
+ attr_accessor :parameters, :receiver, :static
70
66
 
71
67
  class << self
72
68
  def build_from_invocation(mc = MethodCall.new, defined_class, method, receiver, arguments)
@@ -88,6 +84,7 @@ module AppMap
88
84
  object_id: receiver.__id__,
89
85
  value: display_string(receiver)
90
86
  }
87
+ mc.static = receiver.is_a?(Module)
91
88
  MethodEvent.build_from_invocation(mc, :call, defined_class, method)
92
89
  end
93
90
  end
@@ -95,10 +92,13 @@ module AppMap
95
92
 
96
93
  def to_h
97
94
  super.tap do |h|
95
+ h[:static] = static
98
96
  h[:parameters] = parameters
99
97
  h[:receiver] = receiver
100
98
  end
101
99
  end
100
+
101
+ alias static? static
102
102
  end
103
103
 
104
104
  class MethodReturnIgnoreValue < MethodEvent
@@ -6,147 +6,125 @@ module AppMap
6
6
  class Hook
7
7
  LOG = false
8
8
 
9
- Package = Struct.new(:path, :exclude) do
10
- def to_h
11
- {
12
- path: path,
13
- exclude: exclude.blank? ? nil : exclude
14
- }.compact
9
+ HOOK_DISABLE_KEY = 'AppMap::Hook.disable'
10
+
11
+ class << self
12
+ # Return the class, separator ('.' or '#'), and method name for
13
+ # the given method.
14
+ def qualify_method_name(method)
15
+ if method.owner.singleton_class?
16
+ # Singleton class names can take two forms:
17
+ # #<Class:Foo> or
18
+ # #<Class:#<Bar:0x0123ABC>>. Retrieve the name of
19
+ # the class from the string.
20
+ #
21
+ # (There really isn't a better way to do this. The
22
+ # singleton's reference to the class it was created
23
+ # from is stored in an instance variable named
24
+ # '__attached__'. It doesn't have the '@' prefix, so
25
+ # it's internal only, and not accessible from user
26
+ # code.)
27
+ class_name = /#<Class:((#<(?<cls>.*?):)|((?<cls>.*?)>))/.match(method.owner.to_s)['cls']
28
+ [ class_name, '.', method.name ]
29
+ else
30
+ [ method.owner.name, '#', method.name ]
31
+ end
15
32
  end
16
33
  end
17
34
 
18
- Config = Struct.new(:name, :packages) do
19
- class << self
20
- # Loads configuration data from a file, specified by the file name.
21
- def load_from_file(config_file_name)
22
- require 'yaml'
23
- load YAML.safe_load(::File.read(config_file_name))
24
- end
35
+ attr_reader :config
36
+ def initialize(config)
37
+ @config = config
38
+ end
25
39
 
26
- # Loads configuration from a Hash.
27
- def load(config_data)
28
- packages = (config_data['packages'] || []).map do |package|
29
- Package.new(package['path'], package['exclude'] || [])
30
- end
31
- Config.new config_data['name'], packages
32
- end
40
+ # Observe class loading and hook all methods which match the config.
41
+ def enable &block
42
+ before_hook = lambda do |defined_class, method, receiver, args|
43
+ require 'appmap/event'
44
+ call_event = AppMap::Event::MethodCall.build_from_invocation(defined_class, method, receiver, args)
45
+ AppMap.tracing.record_event call_event, defined_class: defined_class, method: method
46
+ [ call_event, Time.now ]
33
47
  end
34
48
 
35
- def initialize(name, packages = [])
36
- super name, packages || []
49
+ after_hook = lambda do |call_event, defined_class, method, start_time, return_value, exception|
50
+ require 'appmap/event'
51
+ elapsed = Time.now - start_time
52
+ return_event = AppMap::Event::MethodReturn.build_from_invocation \
53
+ defined_class, method, call_event.id, elapsed, return_value, exception
54
+ AppMap.tracing.record_event return_event
37
55
  end
38
56
 
39
- def to_h
40
- {
41
- name: name,
42
- packages: packages.map(&:to_h)
43
- }
57
+ with_disabled_hook = lambda do |&fn|
58
+ # Don't record functions, such as to_s and inspect, that might be called
59
+ # by the fn. Otherwise there can be a stack overflow.
60
+ Thread.current[HOOK_DISABLE_KEY] = true
61
+ begin
62
+ fn.call
63
+ ensure
64
+ Thread.current[HOOK_DISABLE_KEY] = false
65
+ end
44
66
  end
45
- end
46
67
 
47
- HOOK_DISABLE_KEY = 'AppMap::Hook.disable'
68
+ tp = TracePoint.new(:end) do |tp|
69
+ hook = self
70
+ cls = tp.self
48
71
 
49
- class << self
50
- # Observe class loading and hook all methods which match the config.
51
- def hook(config = AppMap.configure)
52
- package_include_paths = config.packages.map(&:path)
53
- package_exclude_paths = config.packages.map do |pkg|
54
- pkg.exclude.map do |exclude|
55
- File.join(pkg.path, exclude)
56
- end
57
- end.flatten
72
+ instance_methods = cls.public_instance_methods(false)
73
+ class_methods = cls.singleton_class.public_instance_methods(false) - instance_methods
58
74
 
59
- before_hook = lambda do |defined_class, method, receiver, args|
60
- require 'appmap/event'
61
- call_event = AppMap::Event::MethodCall.build_from_invocation(defined_class, method, receiver, args)
62
- AppMap.tracing.record_event call_event, defined_class: defined_class, method: method
63
- [ call_event, Time.now ]
64
- end
75
+ hook_method = lambda do |cls|
76
+ lambda do |method_id|
77
+ next if method_id.to_s =~ /_hooked_by_appmap$/
65
78
 
66
- after_hook = lambda do |call_event, defined_class, method, start_time, return_value, exception|
67
- require 'appmap/event'
68
- elapsed = Time.now - start_time
69
- return_event = AppMap::Event::MethodReturn.build_from_invocation \
70
- defined_class, method, call_event.id, elapsed, return_value, exception
71
- AppMap.tracing.record_event return_event
72
- end
79
+ method = cls.public_instance_method(method_id)
80
+ disasm = RubyVM::InstructionSequence.disasm(method)
81
+ # Skip methods that have no instruction sequence, as they are obviously trivial.
82
+ next unless disasm
73
83
 
74
- with_disabled_hook = lambda do |&fn|
75
- # Don't record functions, such as to_s and inspect, that might be called
76
- # by the fn. Otherwise there can be a stack oveflow.
77
- Thread.current[HOOK_DISABLE_KEY] = true
78
- begin
79
- fn.call
80
- ensure
81
- Thread.current[HOOK_DISABLE_KEY] = false
82
- end
83
- end
84
+ defined_class, method_symbol, method_name = Hook.qualify_method_name(method)
85
+ method_display_name = [defined_class, method_symbol, method_name].join
84
86
 
85
- TracePoint.trace(:end) do |tp|
86
- cls = tp.self
87
-
88
- instance_methods = cls.public_instance_methods(false)
89
- class_methods = cls.singleton_class.public_instance_methods(false) - instance_methods
90
-
91
- hook_method = lambda do |cls|
92
- lambda do |method_id|
93
- next if method_id.to_s =~ /_hooked_by_appmap$/
94
-
95
- method = cls.public_instance_method(method_id)
96
- location = method.source_location
97
- location_file, = location
98
- next unless location_file
99
-
100
- location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
101
- match = package_include_paths.find { |p| location_file.index(p) == 0 }
102
- match &&= !package_exclude_paths.find { |p| location_file.index(p) }
103
- next unless match
104
-
105
- disasm = RubyVM::InstructionSequence.disasm(method)
106
- # Skip methods that have no instruction sequence, as they are obviously trivial.
107
- next unless disasm
108
-
109
- defined_class, method_symbol = \
110
- if method.owner.singleton_class?
111
- # Singleton class name is like: #<Class:<(.*)>>
112
- class_name = method.owner.to_s['#<Class:<'.length-1..-2]
113
- [ class_name, '.' ]
114
- else
115
- [ method.owner.name, '#' ]
116
- end
87
+ # Don't try and trace the AppMap methods or there will be
88
+ # a stack overflow in the defined hook method.
89
+ next if /\AAppMap[:\.]/.match?(method_display_name)
117
90
 
118
- warn "AppMap: Hooking #{defined_class}#{method_symbol}#{method.name}" if LOG
91
+ next unless \
92
+ config.always_hook?(defined_class, method_name) ||
93
+ config.included_by_location?(method)
119
94
 
120
- cls.define_method method_id do |*args, &block|
121
- base_method = method.bind(self).to_proc
95
+ warn "AppMap: Hooking #{method_display_name}" if LOG
122
96
 
123
- hook_disabled = Thread.current[HOOK_DISABLE_KEY]
124
- enabled = true if !hook_disabled && AppMap.tracing.enabled?
125
- return base_method.call(*args, &block) unless enabled
97
+ cls.define_method method_id do |*args, &block|
98
+ base_method = method.bind(self).to_proc
126
99
 
127
- call_event, start_time = with_disabled_hook.call do
128
- before_hook.call(defined_class, method, self, args)
129
- end
130
- return_value = nil
131
- exception = nil
132
- begin
133
- return_value = base_method.call(*args, &block)
134
- rescue
135
- exception = $ERROR_INFO
136
- raise
137
- ensure
138
- with_disabled_hook.call do
139
- after_hook.call(call_event, defined_class, method, start_time, return_value, exception)
140
- end
100
+ hook_disabled = Thread.current[HOOK_DISABLE_KEY]
101
+ enabled = true if !hook_disabled && AppMap.tracing.enabled?
102
+ return base_method.call(*args, &block) unless enabled
103
+
104
+ call_event, start_time = with_disabled_hook.call do
105
+ before_hook.call(defined_class, method, self, args)
106
+ end
107
+ return_value = nil
108
+ exception = nil
109
+ begin
110
+ return_value = base_method.call(*args, &block)
111
+ rescue
112
+ exception = $ERROR_INFO
113
+ raise
114
+ ensure
115
+ with_disabled_hook.call do
116
+ after_hook.call(call_event, defined_class, method, start_time, return_value, exception)
141
117
  end
142
118
  end
143
119
  end
144
120
  end
145
-
146
- instance_methods.each(&hook_method.call(cls))
147
- class_methods.each(&hook_method.call(cls.singleton_class))
148
121
  end
122
+
123
+ instance_methods.each(&hook_method.call(cls))
124
+ class_methods.each(&hook_method.call(cls.singleton_class))
149
125
  end
126
+
127
+ tp.enable(&block)
150
128
  end
151
129
  end
152
130
  end