Class: AppMap::Hook

Inherits:
Object show all
Defined in:
lib/appmap/hook.rb,
lib/appmap/hook/method.rb,
lib/appmap/hook/method/ruby2.rb,
lib/appmap/hook/method/ruby3.rb,
lib/appmap/hook/record_around.rb,
ext/appmap/appmap.c

Defined Under Namespace

Modules: RecordAround Classes: Method

Constant Summary collapse

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
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
SLOW_PACKAGE_THRESHOLD =
0.001
SIGNATURES =
{}
LOOKUP_SIGNATURE =
lambda do |id|
  method = super(id)

  hash_key = Hook.method_hash_key(method.owner, method)
  return method unless hash_key

  signature = SIGNATURES[hash_key]
  if signature
    method.singleton_class.module_eval do
      define_method(:parameters) do
        signature
      end
    end
  end

  method
end

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(config) ⇒ Hook

Returns a new instance of Hook.



57
58
59
60
# File 'lib/appmap/hook.rb', line 57

def initialize(config)
  @config = config
  @trace_enabled = []
end

Instance Attribute Details

#configObject (readonly)

Returns the value of attribute config.



55
56
57
# File 'lib/appmap/hook.rb', line 55

def config
  @config
end

Class Method Details

.already_hooked?(method) ⇒ Boolean

Returns:

  • (Boolean)


35
36
37
38
39
40
41
# File 'lib/appmap/hook.rb', line 35

def already_hooked?(method)
  # After a method is defined, the statement "module_function <the-method>" can convert that method
  # into a module (class) method. The method is hooked first when it's defined, then AppMap will attempt to
  # hook it again when it's redefined as a module method. So we check the method source location - if it's
  # part of the AppMap source tree, we ignore it.
  method.source_location && method.source_location[0].index(__dir__) == 0
end

.hook_builtins?Boolean

Returns:

  • (Boolean)


24
25
26
27
28
29
30
31
32
33
# File 'lib/appmap/hook.rb', line 24

def hook_builtins?
  Mutex.new.synchronize do
    @hook_builtins = true if @hook_builtins.nil?

    next false unless @hook_builtins

    @hook_builtins = false
    true
  end
end

.method_hash_key(cls, method) ⇒ Object



8
9
10
11
12
# File 'lib/appmap/hook/method.rb', line 8

def method_hash_key(cls, method)
  [cls, method.name].hash
rescue TypeError => e
  warn "Error building hash key for #{cls}, #{method}: #{e}"
end

.qualify_method_name(method) ⇒ Object

Return the class, separator (‘.’ or ‘#’), and method name for the given method.



45
46
47
48
49
50
51
52
# File 'lib/appmap/hook.rb', line 45

def qualify_method_name(method)
  if method.owner.singleton_class?
    class_name = singleton_method_owner_name(method)
    [class_name, ".", method.name]
  else
    [method.owner.name, "#", method.name]
  end
end

.singleton_method_owner_name(method) ⇒ Object



16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# File 'ext/appmap/appmap.c', line 16

static VALUE
singleton_method_owner_name(VALUE klass, VALUE method)
{
  VALUE owner = rb_funcall(method, rb_intern("owner"), 0);
  VALUE attached = rb_ivar_get(owner, rb_intern("__attached__"));
  if (!CLASS_OR_MODULE_P(attached)) {
    attached = rb_funcall(attached, rb_intern("class"), 0);
  }

  // Did __attached__.class return an object that's a Module or a
  // Class?
  if (CLASS_OR_MODULE_P(attached)) {
    // Yup, get it's name
    return rb_mod_name(attached);
  }

  // Nope (which seems weird, but whatever). Fall back to calling
  // #to_s on the method's owner and hope for the best.
  return rb_funcall(owner, rb_intern("to_s"), 0);
}

Instance Method Details

#enable(&block) ⇒ Object

Observe class loading and hook all methods which match the config.



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/appmap/hook.rb', line 63

def enable(&block)
  require "appmap/hook/method"

  hook_builtins

  # Paths that are known to be non-tracing.
  @notrace_paths = Set.new
  # Locations that have already been visited.
  @trace_locations = Set.new
  @module_load_times = Hash.new { |memo, k| memo[k] = 0 }
  @slow_packages = Set.new

  if ENV["APPMAP_PROFILE_HOOK"] == "true"
    dump_times = lambda do
      @module_load_times
        .keys
        .select { |key| !@slow_packages.member?(key) }
        .each do |key|
        elapsed = @module_load_times[key]
        if elapsed >= SLOW_PACKAGE_THRESHOLD
          @slow_packages.add(key)
          warn "AppMap: Package #{key} took #{@module_load_times[key]} seconds to hook"
        end
      end
    end

    at_exit(&dump_times)
    Thread.new do
      while true
        dump_times.call
        sleep 5
      end
    end
  end

  @trace_end = TracePoint.new(:end, &method(:trace_end))
  @trace_end.enable(&block)
end

#hook_builtinsObject

hook_builtins builds hooks for code that is built in to the Ruby standard library. No TracePoint events are emitted for builtins, so a separate hooking mechanism is needed.



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/appmap/hook.rb', line 104

def hook_builtins
  return unless self.class.hook_builtins?

  hook_loaded_code = lambda do |hooks_by_class, builtin|
    hooks_by_class.each do |class_name, hooks|
      Array(hooks).each do |hook|
        HookLog.builtin class_name do
          if builtin && hook.package.require_name && hook.package.require_name != "ruby"
            begin
              require hook.package.require_name
            rescue
              HookLog.load_error hook.package.require_name, "Unable to require #{hook.package.require_name}: #{$!}" if HookLog.enabled?
              next
            end
          end

          begin
            base_cls = Object.const_get class_name
          rescue NameError
            HookLog.load_error class_name, "Class #{class_name} not found in global scope" if HookLog.enabled?
            next
          end

          Array(hook.method_names).each do |method_name|
            method_name = method_name.to_sym

            hook_method = lambda do |entry|
              cls, method = entry
              next if config.never_hook?(cls, method)

              hook.package.handler_class.new(hook.package, cls, method).activate
            end

            methods = []
            # irb(main):001:0> Kernel.public_instance_method(:system)
            # (irb):1:in `public_instance_method': method `system' for module `Kernel' is  private (NameError)
            if base_cls == Kernel
              begin
                methods << [base_cls, base_cls.instance_method(method_name)]
              rescue
                nil
              end
            end
            begin
              methods << [base_cls, base_cls.public_instance_method(method_name)]
            rescue
              nil
            end
            begin
              methods << [base_cls, base_cls.protected_instance_method(method_name)]
            rescue
              nil
            end
            if base_cls.respond_to?(:singleton_class)
              begin
                methods << [base_cls.singleton_class, base_cls.singleton_class.public_instance_method(method_name)]
              rescue
                nil
              end
              begin
                methods << [base_cls.singleton_class, base_cls.singleton_class.protected_instance_method(method_name)]
              rescue
                nil
              end
            end
            methods.compact!
            if methods.empty?
              HookLog.load_error [base_cls.name, method_name].join("[#.]"), "Method #{method_name} not found on #{base_cls.name}" if HookLog.enabled?
            else
              methods.each(&hook_method)
            end
          end
        end
      end
    end
  end

  hook_loaded_code.call(config.builtin_hooks, true)
end