appmap 0.34.4 → 0.36.0

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: a3a509a94faa2a85ff11702ae55ab3df975f04416d93d169b0a24989fd520b9d
4
- data.tar.gz: 3895b9c788413f85738ba487e717bf48e4dfc2e8c0792f30c6f9f6ba7dc5e9c8
3
+ metadata.gz: 102dab80f2d8cffd51fd1377583a1b063078cd77ab7ad4ffdcd20867a084b11b
4
+ data.tar.gz: cbb87144593416f84099cf8a9d8ac18f18d9763dce28277320ae6a99ff59ec58
5
5
  SHA512:
6
- metadata.gz: 07fe201395cad27e731402b5b2ff0efd653056dad16b9301657034df0b6e7fd3a205768ef3656874f55109edfc4d788d752f8bc73f4ff6874c815104547ce281
7
- data.tar.gz: cbbd1551c352519954e9ddc9dd27279d6d232663fb67fd5ed4369a603a382f140593aa195aa08f35a57b0a14d38bef07bfef0aa7bd942d660a865fb3682d500d
6
+ metadata.gz: a24e2ecb22a654d11e91ef28a7fdd961c178144f474a36e26c70e8e243bac18c13e3c5ff606f5ed2a058ddf6d98f9dbd0678a0f986ddfae4f18ed7524c27d41d
7
+ data.tar.gz: 712cb7e18026aa80135af7ce54a7a8c412362355c0eb6b151a0f05354cf991ebe9402e298d9646b9fab2e5d673d3338a773a0b1b1d0e1846ec3ed970074a1f77
@@ -1,3 +1,27 @@
1
+ # v0.36.0
2
+ * *appmap.yml* package definition may specify `gem`.
3
+ * Skip loading the railtie if `APPMAP_INITIALIZE` environment variable
4
+ is set to `false`.
5
+
6
+ # v0.35.2
7
+ * Make sure `MethodEvent#display_string` works when the value's `#to_s` and/or `#inspect`
8
+ methods have problems.
9
+
10
+ # v0.35.1
11
+ * Take out hooking of `IO` and `Logger` methods.
12
+ * Enable logging if either `APPMAP_DEBUG` or `DEBUG` is `true`.
13
+
14
+ # v0.35.0
15
+ * Provide a custom display string for files and HTTP requests.
16
+ * Report `mime_type` on HTTP response.
17
+
18
+ # v0.34.6
19
+ * Only warn once about problems determining database version for an ActiveRecord
20
+ connection.
21
+
22
+ # v0.34.5
23
+ * Ensure that hooking a method doesn't change its arity.
24
+
1
25
  # v0.34.4
2
26
  * Make sure `AppMap:Rails::SQLExaminer::ActiveRecordExaminer.server_version` only calls
3
27
  `ActiveRecord::Base.connection.database_version` if it's available.
data/README.md CHANGED
@@ -57,6 +57,15 @@ end
57
57
 
58
58
  Then install with `bundle`.
59
59
 
60
+ **Railtie**
61
+
62
+ If you are using Ruby on Rails, require the railtie after Rails is loaded.
63
+
64
+ ```
65
+ # application.rb is a good place to do this, along with all the other railties.
66
+ require 'appmap/railtie'
67
+ ```
68
+
60
69
  # Configuration
61
70
 
62
71
  When you run your program, the `appmap` gem reads configuration settings from `appmap.yml`. Here's a sample configuration
@@ -67,6 +76,7 @@ name: MyProject
67
76
  packages:
68
77
  - path: app/controllers
69
78
  - path: app/models
79
+ - gem: activerecord
70
80
  ```
71
81
 
72
82
  * **name** Provides the project name (required)
@@ -78,6 +88,7 @@ Each entry in the `packages` list is a YAML object which has the following keys:
78
88
 
79
89
  * **path** The path to the source code directory. The path may be relative to the current working directory, or it may
80
90
  be an absolute path.
91
+ * **gem** As an alternative to specifying the path, specify the name of a dependency gem. When using `gem`, don't specify `path`.
81
92
  * **exclude** A list of files and directories which will be ignored. By default, all modules, classes and public
82
93
  functions are inspected.
83
94
 
data/Rakefile CHANGED
@@ -113,7 +113,7 @@ namespace :spec do
113
113
  desc ruby_version
114
114
  task ruby_version, [:specs] => ["compile", "build:fixtures:#{ruby_version}:all"] do |_, task_args|
115
115
  run_specs(ruby_version, task_args)
116
- end.tap do|t|
116
+ end.tap do |t|
117
117
  desc "Run all specs"
118
118
  task :all, [:specs] => t
119
119
  end
@@ -30,7 +30,7 @@ Gem::Specification.new do |spec|
30
30
  spec.add_dependency 'parser'
31
31
  spec.add_dependency 'rack'
32
32
 
33
- spec.add_development_dependency 'bundler', '~> 1.16'
33
+ spec.add_development_dependency 'bundler', '>= 1.16'
34
34
  spec.add_development_dependency 'minitest', '~> 5.0'
35
35
  spec.add_development_dependency 'pry-byebug'
36
36
  spec.add_development_dependency 'rake', '>= 12.3.3'
data/appmap.yml CHANGED
@@ -1,8 +1,2 @@
1
1
  name: AppMap Rubygem
2
- packages:
3
- - path: lib/appmap
4
- exclude:
5
- - server
6
- - trace
7
-
8
- - path: examples
2
+ packages: []
@@ -8,7 +8,12 @@
8
8
  (!SPECIAL_CONST_P(obj) && \
9
9
  (BUILTIN_TYPE(obj) == T_CLASS || BUILTIN_TYPE(obj) == T_MODULE))
10
10
 
11
- static VALUE singleton_method_owner_name(VALUE klass, VALUE method)
11
+ #define ARITIES_KEY "__arities__"
12
+
13
+ VALUE am_AppMapHook;
14
+
15
+ static VALUE
16
+ singleton_method_owner_name(VALUE klass, VALUE method)
12
17
  {
13
18
  VALUE owner = rb_funcall(method, rb_intern("owner"), 0);
14
19
  VALUE attached = rb_ivar_get(owner, rb_intern("__attached__"));
@@ -27,10 +32,64 @@ static VALUE singleton_method_owner_name(VALUE klass, VALUE method)
27
32
  // #to_s on the method's owner and hope for the best.
28
33
  return rb_funcall(owner, rb_intern("to_s"), 0);
29
34
  }
30
-
35
+
36
+
37
+ static VALUE
38
+ am_define_method_with_arity(VALUE mod, VALUE name, VALUE arity, VALUE proc)
39
+ {
40
+ VALUE arities_key = rb_intern(ARITIES_KEY);
41
+ VALUE arities = rb_ivar_get(mod, arities_key);
42
+
43
+ if (arities == Qundef || NIL_P(arities)) {
44
+ arities = rb_hash_new();
45
+ rb_ivar_set(mod, arities_key, arities);
46
+ }
47
+ rb_hash_aset(arities, name, arity);
48
+
49
+ return rb_funcall(mod, rb_intern("define_method"), 2, name, proc);
50
+ }
51
+
52
+ static VALUE
53
+ am_get_method_arity(VALUE method, VALUE orig_arity_method)
54
+ {
55
+ VALUE owner = rb_funcall(method, rb_intern("owner"), 0);
56
+ VALUE arities = rb_ivar_get(owner, rb_intern(ARITIES_KEY));
57
+ VALUE name = rb_funcall(method, rb_intern("name"), 0);
58
+ VALUE arity = Qnil;
59
+ // See if we saved an arity for the method.
60
+ if (!NIL_P(arities)) {
61
+ arity = rb_hash_aref(arities, name);
62
+ }
63
+ // Didn't find one, call the original method.
64
+ if (NIL_P(arity)) {
65
+ VALUE bound_method = rb_funcall(orig_arity_method, rb_intern("bind"), 1, method);
66
+ arity = rb_funcall(bound_method, rb_intern("call"), 0);
67
+ }
68
+
69
+ return arity;
70
+ }
71
+
72
+ static VALUE
73
+ am_unbound_method_arity(VALUE method)
74
+ {
75
+ VALUE orig_unbound_method_arity = rb_ivar_get(am_AppMapHook, rb_intern("@unbound_method_arity"));
76
+ return am_get_method_arity(method, orig_unbound_method_arity);
77
+ }
78
+
79
+ static VALUE
80
+ am_method_arity(VALUE method)
81
+ {
82
+ VALUE orig_method_arity = rb_ivar_get(am_AppMapHook, rb_intern("@method_arity"));
83
+ return am_get_method_arity(method, orig_method_arity);
84
+ }
85
+
31
86
  void Init_appmap() {
32
87
  VALUE appmap = rb_define_module("AppMap");
33
- VALUE hook = rb_define_class_under(appmap, "Hook", rb_cObject);
88
+ am_AppMapHook = rb_define_class_under(appmap, "Hook", rb_cObject);
89
+
90
+ rb_define_singleton_method(am_AppMapHook, "singleton_method_owner_name", singleton_method_owner_name, 1);
34
91
 
35
- rb_define_singleton_method(hook, "singleton_method_owner_name", singleton_method_owner_name, 1);
92
+ rb_define_method(rb_cModule, "define_method_with_arity", am_define_method_with_arity, 3);
93
+ rb_define_method(rb_cUnboundMethod, "arity", am_unbound_method_arity, 0);
94
+ rb_define_method(rb_cMethod, "arity", am_method_arity, 0);
36
95
  }
@@ -84,7 +84,7 @@ module AppMap
84
84
 
85
85
  # Builds a class map from a config and a list of Ruby methods.
86
86
  def class_map(methods)
87
- ClassMap.build_from_methods(configuration, methods)
87
+ ClassMap.build_from_methods(methods)
88
88
  end
89
89
 
90
90
  # Returns default metadata detected from the Ruby system and from the
@@ -61,30 +61,29 @@ module AppMap
61
61
  location: location,
62
62
  static: static,
63
63
  labels: labels
64
- }.delete_if { |k,v| v.nil? || v == [] }
64
+ }.delete_if { |_, v| v.nil? || v == [] }
65
65
  end
66
66
  end
67
67
  end
68
68
 
69
69
  class << self
70
- def build_from_methods(config, methods)
70
+ def build_from_methods(methods)
71
71
  root = Types::Root.new
72
72
  methods.each do |method|
73
- package = config.package_for_method(method) \
74
- or raise "No package found for method #{method}"
75
- add_function root, package, method
73
+ add_function root, method
76
74
  end
77
75
  root.children.map(&:to_h)
78
76
  end
79
77
 
80
78
  protected
81
79
 
82
- def add_function(root, package, method)
80
+ def add_function(root, method)
81
+ package = method.package
83
82
  static = method.static
84
83
 
85
84
  object_infos = [
86
85
  {
87
- name: package.path,
86
+ name: package.name,
88
87
  type: 'package'
89
88
  }
90
89
  ]
@@ -2,15 +2,39 @@
2
2
 
3
3
  module AppMap
4
4
  class Config
5
- Package = Struct.new(:path, :package_name, :exclude, :labels) do
6
- def initialize(path, package_name: nil, exclude: [], labels: [])
7
- super path, package_name, exclude, labels
5
+ Package = Struct.new(:path, :gem, :package_name, :exclude, :labels) do
6
+ class << self
7
+ def build_from_path(path, package_name: nil, exclude: [], labels: [])
8
+ Package.new(path, nil, package_name, exclude, labels)
9
+ end
10
+
11
+ def build_from_gem(gem, package_name: nil, exclude: [], labels: [])
12
+ gem_paths(gem).map do |gem_path|
13
+ Package.new(gem_path, gem, package_name, exclude, labels)
14
+ end
15
+ end
16
+
17
+ private_class_method :new
18
+
19
+ protected
20
+
21
+ def gem_paths(gem)
22
+ gemspec = Gem.loaded_specs[gem] or raise "Gem #{gem.inspect} not found"
23
+ gemspec.source_paths.map do |path|
24
+ File.join(gemspec.gem_dir, path)
25
+ end
26
+ end
27
+ end
28
+
29
+ def name
30
+ gem || path
8
31
  end
9
-
32
+
10
33
  def to_h
11
34
  {
12
35
  path: path,
13
36
  package_name: package_name,
37
+ gem: gem,
14
38
  exclude: exclude.blank? ? nil : exclude,
15
39
  labels: labels.blank? ? nil : labels
16
40
  }.compact
@@ -20,31 +44,29 @@ module AppMap
20
44
  Hook = Struct.new(:method_names, :package) do
21
45
  end
22
46
 
23
- OPENSSL_PACKAGE = Package.new('openssl', package_name: 'openssl', labels: %w[security crypto])
47
+ OPENSSL_PACKAGES = Package.build_from_path('openssl', package_name: 'openssl', labels: %w[security crypto])
24
48
 
25
49
  # Methods that should always be hooked, with their containing
26
50
  # package and labels that should be applied to them.
27
51
  HOOKED_METHODS = {
28
- 'ActiveSupport::SecurityUtils' => Hook.new(:secure_compare, Package.new('active_support', package_name: 'active_support', labels: %w[security crypto]))
52
+ 'ActiveSupport::SecurityUtils' => Hook.new(:secure_compare, Package.build_from_path('active_support', package_name: 'active_support', labels: %w[security crypto]))
29
53
  }.freeze
30
54
 
31
55
  BUILTIN_METHODS = {
32
- 'OpenSSL::PKey::PKey' => Hook.new(:sign, OPENSSL_PACKAGE),
33
- 'Digest::Instance' => Hook.new(:digest, OPENSSL_PACKAGE),
34
- 'OpenSSL::X509::Request' => Hook.new(%i[sign verify], OPENSSL_PACKAGE),
35
- 'OpenSSL::PKCS5' => Hook.new(%i[pbkdf2_hmac_sha1 pbkdf2_hmac], OPENSSL_PACKAGE),
36
- 'OpenSSL::Cipher' => Hook.new(%i[encrypt decrypt final], OPENSSL_PACKAGE),
37
- 'OpenSSL::X509::Certificate' => Hook.new(:sign, OPENSSL_PACKAGE),
38
- 'Logger' => Hook.new(:add, Package.new('logger', labels: %w[log io])),
39
- 'Net::HTTP' => Hook.new(:request, Package.new('net/http', package_name: 'net/http', labels: %w[http io])),
40
- 'Net::SMTP' => Hook.new(:send, Package.new('net/smtp', package_name: 'net/smtp', labels: %w[smtp email io])),
41
- 'Net::POP3' => Hook.new(:mails, Package.new('net/pop3', package_name: 'net/pop', labels: %w[pop pop3 email io])),
42
- 'Net::IMAP' => Hook.new(:send_command, Package.new('net/imap', package_name: 'net/imap', labels: %w[imap email io])),
43
- 'IO' => Hook.new(%i[read write open close], Package.new('io', labels: %w[io])),
44
- 'Marshal' => Hook.new(%i[dump load], Package.new('marshal', labels: %w[serialization marshal])),
45
- 'Psych' => Hook.new(%i[dump dump_stream load load_stream parse parse_stream], Package.new('yaml', package_name: 'psych', labels: %w[serialization yaml])),
46
- 'JSON::Ext::Parser' => Hook.new(:parse, Package.new('json', package_name: 'json', labels: %w[serialization json])),
47
- 'JSON::Ext::Generator::State' => Hook.new(:generate, Package.new('json', package_name: 'json', labels: %w[serialization json]))
56
+ 'OpenSSL::PKey::PKey' => Hook.new(:sign, OPENSSL_PACKAGES),
57
+ 'Digest::Instance' => Hook.new(:digest, OPENSSL_PACKAGES),
58
+ 'OpenSSL::X509::Request' => Hook.new(%i[sign verify], OPENSSL_PACKAGES),
59
+ 'OpenSSL::PKCS5' => Hook.new(%i[pbkdf2_hmac_sha1 pbkdf2_hmac], OPENSSL_PACKAGES),
60
+ 'OpenSSL::Cipher' => Hook.new(%i[encrypt decrypt final], OPENSSL_PACKAGES),
61
+ 'OpenSSL::X509::Certificate' => Hook.new(:sign, OPENSSL_PACKAGES),
62
+ 'Net::HTTP' => Hook.new(:request, Package.build_from_path('net/http', package_name: 'net/http', labels: %w[http io])),
63
+ 'Net::SMTP' => Hook.new(:send, Package.build_from_path('net/smtp', package_name: 'net/smtp', labels: %w[smtp email io])),
64
+ 'Net::POP3' => Hook.new(:mails, Package.build_from_path('net/pop3', package_name: 'net/pop', labels: %w[pop pop3 email io])),
65
+ 'Net::IMAP' => Hook.new(:send_command, Package.build_from_path('net/imap', package_name: 'net/imap', labels: %w[imap email io])),
66
+ 'Marshal' => Hook.new(%i[dump load], Package.build_from_path('marshal', labels: %w[serialization marshal])),
67
+ 'Psych' => Hook.new(%i[dump dump_stream load load_stream parse parse_stream], Package.build_from_path('yaml', package_name: 'psych', labels: %w[serialization yaml])),
68
+ 'JSON::Ext::Parser' => Hook.new(:parse, Package.build_from_path('json', package_name: 'json', labels: %w[serialization json])),
69
+ 'JSON::Ext::Generator::State' => Hook.new(:generate, Package.build_from_path('json', package_name: 'json', labels: %w[serialization json]))
48
70
  }.freeze
49
71
 
50
72
  attr_reader :name, :packages
@@ -64,8 +86,16 @@ module AppMap
64
86
  # Loads configuration from a Hash.
65
87
  def load(config_data)
66
88
  packages = (config_data['packages'] || []).map do |package|
67
- Package.new(package['path'], exclude: package['exclude'] || [])
68
- end
89
+ gem = package['gem']
90
+ path = package['path']
91
+ raise 'AppMap package configuration should specify gem or path, not both' if gem && path
92
+
93
+ if gem
94
+ Package.build_from_gem(gem, exclude: package['exclude'] || [])
95
+ else
96
+ [ Package.build_from_path(path, exclude: package['exclude'] || []) ]
97
+ end
98
+ end.flatten
69
99
  Config.new config_data['name'], packages
70
100
  end
71
101
  end
@@ -105,7 +135,7 @@ module AppMap
105
135
  hook = find_hook(defined_class)
106
136
  return nil unless hook
107
137
 
108
- Array(hook.method_names).include?(method_name) ? hook.package : nil
138
+ Array(hook.method_names).include?(method_name) ? hook.package : nil
109
139
  end
110
140
 
111
141
  def find_hook(defined_class)
@@ -31,25 +31,43 @@ module AppMap
31
31
  def display_string(value)
32
32
  return nil unless value
33
33
 
34
+ value_string = custom_display_string(value) || default_display_string(value)
35
+
36
+ (value_string||'')[0...LIMIT].encode('utf-8', invalid: :replace, undef: :replace, replace: '_')
37
+ end
38
+
39
+ protected
40
+
41
+ def custom_display_string(value)
42
+ case value
43
+ when File
44
+ "#{value.class}[path=#{value.path}]"
45
+ when Net::HTTP
46
+ "#{value.class}[#{value.address}:#{value.port}]"
47
+ when Net::HTTPGenericRequest
48
+ "#{value.class}[#{value.method} #{value.path}]"
49
+ end
50
+ rescue StandardError
51
+ nil
52
+ end
53
+
54
+ def default_display_string(value)
34
55
  last_resort_string = lambda do
35
56
  warn "AppMap encountered an error inspecting a #{value.class.name}: #{$!.message}"
36
57
  '*Error inspecting variable*'
37
58
  end
38
59
 
39
- value_string = \
60
+ begin
61
+ value.to_s
62
+ rescue NoMethodError
40
63
  begin
41
- value.to_s
42
- rescue NoMethodError
43
- begin
44
- value.inspect
45
- rescue StandardError
46
- last_resort_string.call
47
- end
64
+ value.inspect
48
65
  rescue StandardError
49
66
  last_resort_string.call
50
67
  end
51
-
52
- (value_string||'')[0...LIMIT].encode('utf-8', invalid: :replace, undef: :replace, replace: '_')
68
+ rescue StandardError
69
+ last_resort_string.call
70
+ end
53
71
  end
54
72
  end
55
73
  end
@@ -4,7 +4,10 @@ require 'English'
4
4
 
5
5
  module AppMap
6
6
  class Hook
7
- LOG = (ENV['DEBUG'] == 'true')
7
+ LOG = (ENV['APPMAP_DEBUG'] == 'true' || ENV['DEBUG'] == 'true')
8
+
9
+ @unbound_method_arity = ::UnboundMethod.instance_method(:arity)
10
+ @method_arity = ::Method.instance_method(:arity)
8
11
 
9
12
  class << self
10
13
  def lock_builtins
@@ -45,7 +48,6 @@ module AppMap
45
48
  hook = lambda do |hook_cls|
46
49
  lambda do |method_id|
47
50
  method = hook_cls.public_instance_method(method_id)
48
- hook_method = Hook::Method.new(hook_cls, method)
49
51
 
50
52
  warn "AppMap: Examining #{hook_cls} #{method.name}" if LOG
51
53
 
@@ -53,14 +55,16 @@ module AppMap
53
55
  # Skip methods that have no instruction sequence, as they are obviously trivial.
54
56
  next unless disasm
55
57
 
56
- # Don't try and trace the AppMap methods or there will be
57
- # a stack overflow in the defined hook method.
58
- next if /\AAppMap[:\.]/.match?(hook_method.method_display_name)
59
-
60
58
  next unless \
61
59
  config.always_hook?(hook_cls, method.name) ||
62
60
  config.included_by_location?(method)
63
61
 
62
+ hook_method = Hook::Method.new(config.package_for_method(method), hook_cls, method)
63
+
64
+ # Don't try and trace the AppMap methods or there will be
65
+ # a stack overflow in the defined hook method.
66
+ next if /\AAppMap[:\.]/.match?(hook_method.method_display_name)
67
+
64
68
  hook_method.activate
65
69
  end
66
70
  end
@@ -94,9 +98,9 @@ module AppMap
94
98
  end
95
99
 
96
100
  if method
97
- Hook::Method.new(cls, method).activate
101
+ Hook::Method.new(hook.package, cls, method).activate
98
102
  else
99
- warn "Method #{method_name} not found on #{cls.name}"
103
+ warn "Method #{method_name} not found on #{cls.name}"
100
104
  end
101
105
  end
102
106
  end