appmap 0.28.1 → 0.34.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 +32 -1
- data/README.md +54 -2
- data/Rakefile +1 -1
- data/appmap.gemspec +2 -0
- data/lib/appmap.rb +25 -14
- data/lib/appmap/class_map.rb +25 -27
- data/lib/appmap/config.rb +115 -0
- data/lib/appmap/cucumber.rb +19 -2
- data/lib/appmap/event.rb +25 -16
- data/lib/appmap/hook.rb +89 -139
- data/lib/appmap/hook/method.rb +83 -0
- data/lib/appmap/metadata.rb +1 -1
- data/lib/appmap/minitest.rb +141 -0
- data/lib/appmap/open.rb +57 -0
- data/lib/appmap/rails/action_handler.rb +7 -7
- data/lib/appmap/rails/sql_handler.rb +10 -8
- data/lib/appmap/record.rb +27 -0
- data/lib/appmap/rspec.rb +2 -2
- data/lib/appmap/trace.rb +16 -8
- data/lib/appmap/util.rb +19 -0
- data/lib/appmap/version.rb +1 -1
- data/spec/abstract_controller4_base_spec.rb +1 -1
- data/spec/abstract_controller_base_spec.rb +9 -2
- data/spec/config_spec.rb +3 -3
- data/spec/fixtures/hook/compare.rb +7 -0
- data/spec/fixtures/hook/instance_method.rb +4 -0
- data/spec/hook_spec.rb +222 -37
- data/spec/open_spec.rb +19 -0
- data/spec/record_sql_rails_pg_spec.rb +56 -33
- data/spec/util_spec.rb +1 -1
- data/test/cli_test.rb +12 -2
- data/test/fixtures/minitest_recorder/Gemfile +5 -0
- data/test/fixtures/minitest_recorder/appmap.yml +3 -0
- data/test/fixtures/minitest_recorder/lib/hello.rb +5 -0
- data/test/fixtures/minitest_recorder/test/hello_test.rb +12 -0
- data/test/fixtures/openssl_recorder/Gemfile +3 -0
- data/test/fixtures/openssl_recorder/appmap.yml +3 -0
- data/test/fixtures/openssl_recorder/lib/openssl_cert_sign.rb +94 -0
- data/test/fixtures/openssl_recorder/lib/openssl_encrypt.rb +34 -0
- data/test/fixtures/openssl_recorder/lib/openssl_key_sign.rb +28 -0
- data/test/fixtures/process_recorder/appmap.yml +3 -0
- data/test/fixtures/process_recorder/hello.rb +9 -0
- data/test/minitest_test.rb +38 -0
- data/test/openssl_test.rb +203 -0
- data/test/record_process_test.rb +35 -0
- data/test/test_helper.rb +1 -0
- metadata +51 -2
data/lib/appmap/cucumber.rb
CHANGED
@@ -4,6 +4,8 @@ require 'appmap/util'
|
|
4
4
|
|
5
5
|
module AppMap
|
6
6
|
module Cucumber
|
7
|
+
APPMAP_OUTPUT_DIR = 'tmp/appmap/cucumber'
|
8
|
+
|
7
9
|
ScenarioAttributes = Struct.new(:name, :feature, :feature_group)
|
8
10
|
|
9
11
|
ProviderStruct = Struct.new(:scenario) do
|
@@ -38,18 +40,27 @@ module AppMap
|
|
38
40
|
end
|
39
41
|
|
40
42
|
class << self
|
43
|
+
def init
|
44
|
+
warn 'Configuring AppMap recorder for Cucumber'
|
45
|
+
|
46
|
+
FileUtils.mkdir_p APPMAP_OUTPUT_DIR
|
47
|
+
end
|
48
|
+
|
41
49
|
def write_scenario(scenario, appmap)
|
42
50
|
appmap['metadata'] = update_metadata(scenario, appmap['metadata'])
|
43
51
|
scenario_filename = AppMap::Util.scenario_filename(appmap['metadata']['name'])
|
44
52
|
|
45
|
-
|
46
|
-
File.write(File.join('tmp/appmap/cucumber', scenario_filename), JSON.generate(appmap))
|
53
|
+
File.write(File.join(APPMAP_OUTPUT_DIR, scenario_filename), JSON.generate(appmap))
|
47
54
|
end
|
48
55
|
|
49
56
|
def enabled?
|
50
57
|
ENV['APPMAP'] == 'true'
|
51
58
|
end
|
52
59
|
|
60
|
+
def run
|
61
|
+
init
|
62
|
+
end
|
63
|
+
|
53
64
|
protected
|
54
65
|
|
55
66
|
def cucumber_version
|
@@ -87,3 +98,9 @@ module AppMap
|
|
87
98
|
end
|
88
99
|
end
|
89
100
|
end
|
101
|
+
|
102
|
+
if AppMap::Cucumber.enabled?
|
103
|
+
require 'appmap'
|
104
|
+
|
105
|
+
AppMap::Cucumber.run
|
106
|
+
end
|
data/lib/appmap/event.rb
CHANGED
@@ -15,21 +15,15 @@ module AppMap
|
|
15
15
|
end
|
16
16
|
end
|
17
17
|
|
18
|
-
MethodEventStruct = Struct.new(:id, :event, :
|
18
|
+
MethodEventStruct = Struct.new(:id, :event, :thread_id)
|
19
19
|
|
20
20
|
class MethodEvent < MethodEventStruct
|
21
21
|
LIMIT = 100
|
22
22
|
|
23
23
|
class << self
|
24
|
-
def build_from_invocation(me, event_type
|
24
|
+
def build_from_invocation(me, event_type)
|
25
25
|
me.id = AppMap::Event.next_id_counter
|
26
26
|
me.event = event_type
|
27
|
-
me.defined_class = defined_class
|
28
|
-
me.method_id = method.name.to_s
|
29
|
-
path = method.source_location[0]
|
30
|
-
path = path[Dir.pwd.length + 1..-1] if path.index(Dir.pwd) == 0
|
31
|
-
me.path = path
|
32
|
-
me.lineno = method.source_location[1]
|
33
27
|
me.thread_id = Thread.current.object_id
|
34
28
|
end
|
35
29
|
|
@@ -58,15 +52,25 @@ module AppMap
|
|
58
52
|
(value_string||'')[0...LIMIT].encode('utf-8', invalid: :replace, undef: :replace, replace: '_')
|
59
53
|
end
|
60
54
|
end
|
61
|
-
|
62
55
|
end
|
63
56
|
|
64
57
|
class MethodCall < MethodEvent
|
65
|
-
attr_accessor :parameters, :receiver, :static
|
58
|
+
attr_accessor :defined_class, :method_id, :path, :lineno, :parameters, :receiver, :static
|
66
59
|
|
67
60
|
class << self
|
68
61
|
def build_from_invocation(mc = MethodCall.new, defined_class, method, receiver, arguments)
|
69
62
|
mc.tap do
|
63
|
+
static = receiver.is_a?(Module)
|
64
|
+
mc.defined_class = defined_class
|
65
|
+
mc.method_id = method.name.to_s
|
66
|
+
if method.source_location
|
67
|
+
path = method.source_location[0]
|
68
|
+
path = path[Dir.pwd.length + 1..-1] if path.index(Dir.pwd) == 0
|
69
|
+
mc.path = path
|
70
|
+
mc.lineno = method.source_location[1]
|
71
|
+
else
|
72
|
+
mc.path = [ defined_class, static ? '.' : '#', method.name ].join
|
73
|
+
end
|
70
74
|
mc.parameters = method.parameters.map.with_index do |method_param, idx|
|
71
75
|
param_type, param_name = method_param
|
72
76
|
param_name ||= 'arg'
|
@@ -84,17 +88,22 @@ module AppMap
|
|
84
88
|
object_id: receiver.__id__,
|
85
89
|
value: display_string(receiver)
|
86
90
|
}
|
87
|
-
mc.static =
|
88
|
-
MethodEvent.build_from_invocation(mc, :call
|
91
|
+
mc.static = static
|
92
|
+
MethodEvent.build_from_invocation(mc, :call)
|
89
93
|
end
|
90
94
|
end
|
91
95
|
end
|
92
96
|
|
93
97
|
def to_h
|
94
98
|
super.tap do |h|
|
99
|
+
h[:defined_class] = defined_class
|
100
|
+
h[:method_id] = method_id
|
101
|
+
h[:path] = path
|
102
|
+
h[:lineno] = lineno
|
95
103
|
h[:static] = static
|
96
104
|
h[:parameters] = parameters
|
97
105
|
h[:receiver] = receiver
|
106
|
+
h.delete_if { |_, v| v.nil? }
|
98
107
|
end
|
99
108
|
end
|
100
109
|
|
@@ -105,11 +114,11 @@ module AppMap
|
|
105
114
|
attr_accessor :parent_id, :elapsed
|
106
115
|
|
107
116
|
class << self
|
108
|
-
def build_from_invocation(mr = MethodReturnIgnoreValue.new,
|
117
|
+
def build_from_invocation(mr = MethodReturnIgnoreValue.new, parent_id, elapsed)
|
109
118
|
mr.tap do |_|
|
110
119
|
mr.parent_id = parent_id
|
111
120
|
mr.elapsed = elapsed
|
112
|
-
MethodEvent.build_from_invocation(mr, :return
|
121
|
+
MethodEvent.build_from_invocation(mr, :return)
|
113
122
|
end
|
114
123
|
end
|
115
124
|
end
|
@@ -126,7 +135,7 @@ module AppMap
|
|
126
135
|
attr_accessor :return_value, :exceptions
|
127
136
|
|
128
137
|
class << self
|
129
|
-
def build_from_invocation(mr = MethodReturn.new,
|
138
|
+
def build_from_invocation(mr = MethodReturn.new, parent_id, elapsed, return_value, exception)
|
130
139
|
mr.tap do |_|
|
131
140
|
if return_value
|
132
141
|
mr.return_value = {
|
@@ -152,7 +161,7 @@ module AppMap
|
|
152
161
|
|
153
162
|
mr.exceptions = exceptions
|
154
163
|
end
|
155
|
-
MethodReturnIgnoreValue.build_from_invocation(mr,
|
164
|
+
MethodReturnIgnoreValue.build_from_invocation(mr, parent_id, elapsed)
|
156
165
|
end
|
157
166
|
end
|
158
167
|
end
|
data/lib/appmap/hook.rb
CHANGED
@@ -4,162 +4,112 @@ require 'English'
|
|
4
4
|
|
5
5
|
module AppMap
|
6
6
|
class Hook
|
7
|
-
LOG =
|
8
|
-
|
9
|
-
Package = Struct.new(:path, :exclude) do
|
10
|
-
def to_h
|
11
|
-
{
|
12
|
-
path: path,
|
13
|
-
exclude: exclude.blank? ? nil : exclude
|
14
|
-
}.compact
|
15
|
-
end
|
16
|
-
end
|
7
|
+
LOG = (ENV['DEBUG'] == 'true')
|
17
8
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
def load_from_file(config_file_name)
|
22
|
-
require 'yaml'
|
23
|
-
load YAML.safe_load(::File.read(config_file_name))
|
24
|
-
end
|
9
|
+
class << self
|
10
|
+
def lock_builtins
|
11
|
+
return if @builtins_hooked
|
25
12
|
|
26
|
-
|
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
|
13
|
+
@builtins_hooked = true
|
33
14
|
end
|
34
15
|
|
35
|
-
|
36
|
-
|
16
|
+
# Return the class, separator ('.' or '#'), and method name for
|
17
|
+
# the given method.
|
18
|
+
def qualify_method_name(method)
|
19
|
+
if method.owner.singleton_class?
|
20
|
+
# Singleton class names can take two forms:
|
21
|
+
# #<Class:Foo> or
|
22
|
+
# #<Class:#<Bar:0x0123ABC>>. Retrieve the name of
|
23
|
+
# the class from the string.
|
24
|
+
#
|
25
|
+
# (There really isn't a better way to do this. The
|
26
|
+
# singleton's reference to the class it was created
|
27
|
+
# from is stored in an instance variable named
|
28
|
+
# '__attached__'. It doesn't have the '@' prefix, so
|
29
|
+
# it's internal only, and not accessible from user
|
30
|
+
# code.)
|
31
|
+
class_name = /#<Class:((#<(?<cls>.*?):)|((?<cls>.*?)>))/.match(method.owner.to_s)['cls']
|
32
|
+
[ class_name, '.', method.name ]
|
33
|
+
else
|
34
|
+
[ method.owner.name, '#', method.name ]
|
35
|
+
end
|
37
36
|
end
|
37
|
+
end
|
38
38
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
packages: packages.map(&:to_h)
|
43
|
-
}
|
44
|
-
end
|
39
|
+
attr_reader :config
|
40
|
+
def initialize(config)
|
41
|
+
@config = config
|
45
42
|
end
|
46
43
|
|
47
|
-
|
44
|
+
# Observe class loading and hook all methods which match the config.
|
45
|
+
def enable &block
|
46
|
+
require 'appmap/hook/method'
|
48
47
|
|
49
|
-
|
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
|
48
|
+
hook_builtins
|
58
49
|
|
59
|
-
|
60
|
-
|
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
|
50
|
+
tp = TracePoint.new(:end) do |trace_point|
|
51
|
+
cls = trace_point.self
|
65
52
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
53
|
+
instance_methods = cls.public_instance_methods(false)
|
54
|
+
class_methods = cls.singleton_class.public_instance_methods(false) - instance_methods
|
55
|
+
|
56
|
+
hook = lambda do |hook_cls|
|
57
|
+
lambda do |method_id|
|
58
|
+
method = hook_cls.public_instance_method(method_id)
|
59
|
+
hook_method = Hook::Method.new(hook_cls, method)
|
60
|
+
|
61
|
+
warn "AppMap: Examining #{hook_method.method_display_name}" if LOG
|
73
62
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
63
|
+
disasm = RubyVM::InstructionSequence.disasm(method)
|
64
|
+
# Skip methods that have no instruction sequence, as they are obviously trivial.
|
65
|
+
next unless disasm
|
66
|
+
|
67
|
+
# Don't try and trace the AppMap methods or there will be
|
68
|
+
# a stack overflow in the defined hook method.
|
69
|
+
next if /\AAppMap[:\.]/.match?(hook_method.method_display_name)
|
70
|
+
|
71
|
+
next unless \
|
72
|
+
config.always_hook?(hook_method.defined_class, method.name) ||
|
73
|
+
config.included_by_location?(method)
|
74
|
+
|
75
|
+
hook_method.activate
|
82
76
|
end
|
83
77
|
end
|
84
78
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
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 = if method.owner.singleton_class?
|
110
|
-
# Singleton class names can take two forms:
|
111
|
-
# #<Class:Foo> or
|
112
|
-
# #<Class:#<Bar:0x0123ABC>>. Retrieve the name of
|
113
|
-
# the class from the string.
|
114
|
-
#
|
115
|
-
# (There really isn't a better way to do this. The
|
116
|
-
# singleton's reference to the class it was created
|
117
|
-
# from is stored in an instance variable named
|
118
|
-
# '__attached__'. It doesn't have the '@' prefix, so
|
119
|
-
# it's internal only, and not accessible from user
|
120
|
-
# code.)
|
121
|
-
class_name = /#<Class:((#<(?<cls>.*?):)|((?<cls>.*?)>))/.match(method.owner.to_s)['cls']
|
122
|
-
[ class_name, '.' ]
|
123
|
-
else
|
124
|
-
[ method.owner.name, '#' ]
|
125
|
-
end
|
126
|
-
|
127
|
-
method_display_name = "#{defined_class}#{method_symbol}#{method.name}"
|
128
|
-
# Don't try and trace the tracing method or there will be a stack overflow
|
129
|
-
# in the defined hook method.
|
130
|
-
next if method_display_name == "AppMap.tracing"
|
131
|
-
|
132
|
-
warn "AppMap: Hooking #{method_display_name}" if LOG
|
133
|
-
|
134
|
-
cls.define_method method_id do |*args, &block|
|
135
|
-
base_method = method.bind(self).to_proc
|
136
|
-
|
137
|
-
hook_disabled = Thread.current[HOOK_DISABLE_KEY]
|
138
|
-
enabled = true if !hook_disabled && AppMap.tracing.enabled?
|
139
|
-
return base_method.call(*args, &block) unless enabled
|
140
|
-
|
141
|
-
call_event, start_time = with_disabled_hook.call do
|
142
|
-
before_hook.call(defined_class, method, self, args)
|
143
|
-
end
|
144
|
-
return_value = nil
|
145
|
-
exception = nil
|
146
|
-
begin
|
147
|
-
return_value = base_method.call(*args, &block)
|
148
|
-
rescue
|
149
|
-
exception = $ERROR_INFO
|
150
|
-
raise
|
151
|
-
ensure
|
152
|
-
with_disabled_hook.call do
|
153
|
-
after_hook.call(call_event, defined_class, method, start_time, return_value, exception)
|
154
|
-
end
|
155
|
-
end
|
156
|
-
end
|
157
|
-
end
|
158
|
-
end
|
79
|
+
instance_methods.each(&hook.(cls))
|
80
|
+
class_methods.each(&hook.(cls.singleton_class))
|
81
|
+
end
|
82
|
+
|
83
|
+
tp.enable(&block)
|
84
|
+
end
|
159
85
|
|
160
|
-
|
161
|
-
|
86
|
+
def hook_builtins
|
87
|
+
return unless self.class.lock_builtins
|
88
|
+
|
89
|
+
class_from_string = lambda do |fq_class|
|
90
|
+
fq_class.split('::').inject(Object) do |mod, class_name|
|
91
|
+
mod.const_get(class_name)
|
92
|
+
end
|
162
93
|
end
|
94
|
+
|
95
|
+
Config::BUILTIN_METHODS.each do |class_name, hook|
|
96
|
+
require hook.package.package_name if hook.package.package_name
|
97
|
+
Array(hook.method_names).each do |method_name|
|
98
|
+
method_name = method_name.to_sym
|
99
|
+
cls = class_from_string.(class_name)
|
100
|
+
method = \
|
101
|
+
begin
|
102
|
+
cls.instance_method(method_name)
|
103
|
+
rescue NameError
|
104
|
+
cls.method(method_name) rescue nil
|
105
|
+
end
|
106
|
+
|
107
|
+
if method
|
108
|
+
Hook::Method.new(cls, method).activate
|
109
|
+
else
|
110
|
+
warn "Method #{method_name} not found on #{cls.name}"
|
111
|
+
end
|
112
|
+
end
|
163
113
|
end
|
164
114
|
end
|
165
115
|
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module AppMap
|
2
|
+
class Hook
|
3
|
+
class Method
|
4
|
+
attr_reader :hook_class, :hook_method, :defined_class, :method_display_name
|
5
|
+
|
6
|
+
HOOK_DISABLE_KEY = 'AppMap::Hook.disable'
|
7
|
+
private_constant :HOOK_DISABLE_KEY
|
8
|
+
|
9
|
+
# Grab the definition of Time.now here, to avoid interfering
|
10
|
+
# with the method we're hooking.
|
11
|
+
TIME_NOW = Time.method(:now)
|
12
|
+
private_constant :TIME_NOW
|
13
|
+
|
14
|
+
def initialize(hook_class, hook_method)
|
15
|
+
@hook_class = hook_class
|
16
|
+
@hook_method = hook_method
|
17
|
+
@defined_class, method_symbol = Hook.qualify_method_name(@hook_method)
|
18
|
+
@method_display_name = [@defined_class, method_symbol, @hook_method.name].join
|
19
|
+
end
|
20
|
+
|
21
|
+
def activate
|
22
|
+
warn "AppMap: Hooking #{method_display_name}" if Hook::LOG
|
23
|
+
|
24
|
+
hook_method = self.hook_method
|
25
|
+
before_hook = self.method(:before_hook)
|
26
|
+
after_hook = self.method(:after_hook)
|
27
|
+
with_disabled_hook = self.method(:with_disabled_hook)
|
28
|
+
|
29
|
+
hook_class.define_method hook_method.name do |*args, &block|
|
30
|
+
instance_method = hook_method.bind(self).to_proc
|
31
|
+
|
32
|
+
hook_disabled = Thread.current[HOOK_DISABLE_KEY]
|
33
|
+
enabled = true if !hook_disabled && AppMap.tracing.enabled?
|
34
|
+
return instance_method.call(*args, &block) unless enabled
|
35
|
+
|
36
|
+
call_event, start_time = with_disabled_hook.() do
|
37
|
+
before_hook.(self, args)
|
38
|
+
end
|
39
|
+
return_value = nil
|
40
|
+
exception = nil
|
41
|
+
begin
|
42
|
+
return_value = instance_method.(*args, &block)
|
43
|
+
rescue
|
44
|
+
exception = $ERROR_INFO
|
45
|
+
raise
|
46
|
+
ensure
|
47
|
+
with_disabled_hook.() do
|
48
|
+
after_hook.(call_event, start_time, return_value, exception)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
protected
|
55
|
+
|
56
|
+
def before_hook(receiver, args)
|
57
|
+
require 'appmap/event'
|
58
|
+
call_event = AppMap::Event::MethodCall.build_from_invocation(defined_class, hook_method, receiver, args)
|
59
|
+
AppMap.tracing.record_event call_event, defined_class: defined_class, method: hook_method
|
60
|
+
[ call_event, TIME_NOW.call ]
|
61
|
+
end
|
62
|
+
|
63
|
+
def after_hook(call_event, start_time, return_value, exception)
|
64
|
+
require 'appmap/event'
|
65
|
+
elapsed = TIME_NOW.call - start_time
|
66
|
+
return_event = \
|
67
|
+
AppMap::Event::MethodReturn.build_from_invocation call_event.id, elapsed, return_value, exception
|
68
|
+
AppMap.tracing.record_event return_event
|
69
|
+
end
|
70
|
+
|
71
|
+
def with_disabled_hook(&fn)
|
72
|
+
# Don't record functions, such as to_s and inspect, that might be called
|
73
|
+
# by the fn. Otherwise there can be a stack overflow.
|
74
|
+
Thread.current[HOOK_DISABLE_KEY] = true
|
75
|
+
begin
|
76
|
+
fn.call
|
77
|
+
ensure
|
78
|
+
Thread.current[HOOK_DISABLE_KEY] = false
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|