appmap 0.31.0 → 0.32.0
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 +4 -0
- data/README.md +30 -2
- data/lib/appmap.rb +1 -0
- data/lib/appmap/class_map.rb +7 -6
- data/lib/appmap/config.rb +11 -13
- data/lib/appmap/event.rb +25 -16
- data/lib/appmap/hook.rb +16 -66
- data/lib/appmap/hook/method.rb +78 -0
- data/lib/appmap/rails/action_handler.rb +7 -7
- data/lib/appmap/rails/sql_handler.rb +10 -8
- data/lib/appmap/trace.rb +7 -7
- 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/hook_spec.rb +5 -37
- data/spec/record_sql_rails_pg_spec.rb +56 -33
- data/test/cli_test.rb +2 -2
- data/test/test_helper.rb +1 -0
- metadata +3 -3
- data/spec/fixtures/hook/openssl_sign.rb +0 -87
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f6becd9530caf01e14b8a7db69e74b40297d41e4724f25315cd7b44ea6ad3e02
|
4
|
+
data.tar.gz: 2863e714f0651d73fc9f2f87d0ea2c5f9fb7f9f553d1f109d9d8f10fff35f5be
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a5574478fee68b2531449df2b9ec135a60110950cdcd518cf5b85d10e6d0d756b3e22740a4ce0905d7fccb2e6e7d684ee38ac61fd11ad6fdfcff4355a7ae7b36
|
7
|
+
data.tar.gz: d15827a809ffde6b6ff98bfb62057f759cdc03e7e24452c1a8273801f94e37d61bab265ddb863ce8f60d83c639875fcf2d247f27abe9b27164817f0739b7c699
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -33,7 +33,7 @@ There are several ways to record AppMaps of your Ruby program using the `appmap`
|
|
33
33
|
|
34
34
|
Once you have recorded some AppMaps (for example, by running RSpec tests), you use the `appland upload` command
|
35
35
|
to upload them to the AppLand server. This command, and some others, is provided
|
36
|
-
by the [AppLand CLI](https://github.com/applandinc/appland-cli/releases)
|
36
|
+
by the [AppLand CLI](https://github.com/applandinc/appland-cli/releases).
|
37
37
|
Then, on the [AppLand website](https://app.land), you can
|
38
38
|
visualize the design of your code and share links with collaborators.
|
39
39
|
|
@@ -87,12 +87,25 @@ Each entry in the `packages` list is a YAML object which has the following keys:
|
|
87
87
|
|
88
88
|
To record RSpec tests, follow these additional steps:
|
89
89
|
|
90
|
-
1) Require `appmap/rspec` in your `spec_helper.rb
|
90
|
+
1) Require `appmap/rspec` in your `spec_helper.rb` before any other classes are loaded.
|
91
91
|
|
92
92
|
```ruby
|
93
93
|
require 'appmap/rspec'
|
94
94
|
```
|
95
95
|
|
96
|
+
Note that `spec_helper.rb` in a Rails project typically loads the application's classes this way:
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
require File.expand_path("../../config/environment", __FILE__)
|
100
|
+
```
|
101
|
+
|
102
|
+
and `appmap/rspec` must be required before this:
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
require 'appmap/rspec'
|
106
|
+
require File.expand_path("../../config/environment", __FILE__)
|
107
|
+
```
|
108
|
+
|
96
109
|
2) *Optional* Add `feature: '<feature name>'` and `feature_group: '<feature group name>'` annotations to your
|
97
110
|
examples.
|
98
111
|
|
@@ -136,6 +149,19 @@ To record Minitest tests, follow these additional steps:
|
|
136
149
|
require 'appmap/minitest'
|
137
150
|
```
|
138
151
|
|
152
|
+
Note that `test_helper.rb` in a Rails project typically loads the application's classes this way:
|
153
|
+
|
154
|
+
```ruby
|
155
|
+
require_relative '../config/environment'
|
156
|
+
```
|
157
|
+
|
158
|
+
and `appmap/rspec` must be required before this:
|
159
|
+
|
160
|
+
```ruby
|
161
|
+
require 'appmap/rspec'
|
162
|
+
require_relative '../config/environment'
|
163
|
+
```
|
164
|
+
|
139
165
|
2) Run the tests with the environment variable `APPMAP=true`:
|
140
166
|
|
141
167
|
```sh-session
|
@@ -159,6 +185,8 @@ To record Cucumber tests, follow these additional steps:
|
|
159
185
|
require 'appmap/cucumber'
|
160
186
|
```
|
161
187
|
|
188
|
+
Be sure to require it before `config/environment` is required.
|
189
|
+
|
162
190
|
2) Create an `Around` hook in `support/hooks.rb` to record the scenario:
|
163
191
|
|
164
192
|
|
data/lib/appmap.rb
CHANGED
data/lib/appmap/class_map.rb
CHANGED
@@ -80,10 +80,6 @@ module AppMap
|
|
80
80
|
protected
|
81
81
|
|
82
82
|
def add_function(root, package, method)
|
83
|
-
location = method.source_location
|
84
|
-
location_file, lineno = location
|
85
|
-
location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
|
86
|
-
|
87
83
|
static = method.static
|
88
84
|
|
89
85
|
object_infos = [
|
@@ -101,12 +97,17 @@ module AppMap
|
|
101
97
|
function_info = {
|
102
98
|
name: method.name,
|
103
99
|
type: 'function',
|
104
|
-
location: [ location_file, lineno ].join(':'),
|
105
100
|
static: static
|
106
101
|
}
|
102
|
+
location = method.source_location
|
103
|
+
if location
|
104
|
+
location_file, lineno = location
|
105
|
+
location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
|
106
|
+
function_info[:location] = [ location_file, lineno ].join(':')
|
107
|
+
end
|
107
108
|
function_info[:labels] = package.labels if package.labels
|
108
109
|
object_infos << function_info
|
109
|
-
|
110
|
+
|
110
111
|
parent = root
|
111
112
|
object_infos.each do |info|
|
112
113
|
parent = find_or_create parent.children, info do
|
data/lib/appmap/config.rb
CHANGED
@@ -1,14 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module AppMap
|
4
|
-
Package = Struct.new(:path, :exclude, :labels) do
|
5
|
-
def initialize(path, exclude, labels = nil)
|
4
|
+
Package = Struct.new(:path, :package_name, :exclude, :labels) do
|
5
|
+
def initialize(path, package_name, exclude, labels = nil)
|
6
6
|
super
|
7
7
|
end
|
8
|
-
|
8
|
+
|
9
9
|
def to_h
|
10
10
|
{
|
11
11
|
path: path,
|
12
|
+
package_name: package_name,
|
12
13
|
exclude: exclude.blank? ? nil : exclude,
|
13
14
|
labels: labels.blank? ? nil : labels
|
14
15
|
}.compact
|
@@ -20,10 +21,7 @@ module AppMap
|
|
20
21
|
# package and labels that should be applied to them.
|
21
22
|
HOOKED_METHODS = {
|
22
23
|
'ActiveSupport::SecurityUtils' => {
|
23
|
-
secure_compare: Package.new('active_support', nil, ['security'])
|
24
|
-
},
|
25
|
-
'OpenSSL::X509::Certificate' => {
|
26
|
-
sign: Package.new('openssl', nil, ['security'])
|
24
|
+
secure_compare: Package.new('active_support', nil, nil, ['security'])
|
27
25
|
}
|
28
26
|
}
|
29
27
|
|
@@ -43,7 +41,7 @@ module AppMap
|
|
43
41
|
# Loads configuration from a Hash.
|
44
42
|
def load(config_data)
|
45
43
|
packages = (config_data['packages'] || []).map do |package|
|
46
|
-
Package.new(package['path'], package['exclude'] || [])
|
44
|
+
Package.new(package['path'], nil, package['exclude'] || [])
|
47
45
|
end
|
48
46
|
Config.new config_data['name'], packages
|
49
47
|
end
|
@@ -57,14 +55,14 @@ module AppMap
|
|
57
55
|
end
|
58
56
|
|
59
57
|
def package_for_method(method)
|
58
|
+
defined_class, _, method_name = Hook.qualify_method_name(method)
|
59
|
+
hooked_method = find_hooked_method(defined_class, method_name)
|
60
|
+
return hooked_method if hooked_method
|
61
|
+
|
60
62
|
location = method.source_location
|
61
63
|
location_file, = location
|
62
64
|
return unless location_file
|
63
65
|
|
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
66
|
location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
|
69
67
|
packages.find do |pkg|
|
70
68
|
(location_file.index(pkg.path) == 0) &&
|
@@ -83,7 +81,7 @@ module AppMap
|
|
83
81
|
def find_hooked_method(defined_class, method_name)
|
84
82
|
find_hooked_class(defined_class)[method_name]
|
85
83
|
end
|
86
|
-
|
84
|
+
|
87
85
|
def find_hooked_class(defined_class)
|
88
86
|
HOOKED_METHODS[defined_class] || {}
|
89
87
|
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,9 +4,7 @@ require 'English'
|
|
4
4
|
|
5
5
|
module AppMap
|
6
6
|
class Hook
|
7
|
-
LOG =
|
8
|
-
|
9
|
-
HOOK_DISABLE_KEY = 'AppMap::Hook.disable'
|
7
|
+
LOG = (ENV['DEBUG'] == 'true')
|
10
8
|
|
11
9
|
class << self
|
12
10
|
# Return the class, separator ('.' or '#'), and method name for
|
@@ -17,7 +15,7 @@ module AppMap
|
|
17
15
|
# #<Class:Foo> or
|
18
16
|
# #<Class:#<Bar:0x0123ABC>>. Retrieve the name of
|
19
17
|
# the class from the string.
|
20
|
-
#
|
18
|
+
#
|
21
19
|
# (There really isn't a better way to do this. The
|
22
20
|
# singleton's reference to the class it was created
|
23
21
|
# from is stored in an instance variable named
|
@@ -39,89 +37,41 @@ module AppMap
|
|
39
37
|
|
40
38
|
# Observe class loading and hook all methods which match the config.
|
41
39
|
def enable &block
|
42
|
-
|
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 ]
|
47
|
-
end
|
48
|
-
|
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
|
55
|
-
end
|
56
|
-
|
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
|
66
|
-
end
|
40
|
+
require 'appmap/hook/method'
|
67
41
|
|
68
|
-
tp = TracePoint.new(:end) do |
|
69
|
-
|
70
|
-
cls = tp.self
|
42
|
+
tp = TracePoint.new(:end) do |trace_point|
|
43
|
+
cls = trace_point.self
|
71
44
|
|
72
45
|
instance_methods = cls.public_instance_methods(false)
|
73
46
|
class_methods = cls.singleton_class.public_instance_methods(false) - instance_methods
|
74
47
|
|
75
|
-
|
48
|
+
hook = lambda do |hook_cls|
|
76
49
|
lambda do |method_id|
|
77
50
|
next if method_id.to_s =~ /_hooked_by_appmap$/
|
78
51
|
|
79
|
-
method =
|
52
|
+
method = hook_cls.public_instance_method(method_id)
|
53
|
+
hook_method = Hook::Method.new(hook_cls, method)
|
54
|
+
|
55
|
+
warn "AppMap: Examining #{hook_method.method_display_name}" if LOG
|
56
|
+
|
80
57
|
disasm = RubyVM::InstructionSequence.disasm(method)
|
81
58
|
# Skip methods that have no instruction sequence, as they are obviously trivial.
|
82
59
|
next unless disasm
|
83
60
|
|
84
|
-
defined_class, method_symbol, method_name = Hook.qualify_method_name(method)
|
85
|
-
method_display_name = [defined_class, method_symbol, method_name].join
|
86
|
-
|
87
61
|
# Don't try and trace the AppMap methods or there will be
|
88
62
|
# a stack overflow in the defined hook method.
|
89
|
-
next if /\AAppMap[:\.]/.match?(method_display_name)
|
63
|
+
next if /\AAppMap[:\.]/.match?(hook_method.method_display_name)
|
90
64
|
|
91
65
|
next unless \
|
92
|
-
config.always_hook?(defined_class,
|
66
|
+
config.always_hook?(hook_method.defined_class, method.name) ||
|
93
67
|
config.included_by_location?(method)
|
94
68
|
|
95
|
-
|
96
|
-
|
97
|
-
cls.define_method method_id do |*args, &block|
|
98
|
-
base_method = method.bind(self).to_proc
|
99
|
-
|
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)
|
117
|
-
end
|
118
|
-
end
|
119
|
-
end
|
69
|
+
hook_method.activate
|
120
70
|
end
|
121
71
|
end
|
122
72
|
|
123
|
-
instance_methods.each(&
|
124
|
-
class_methods.each(&
|
73
|
+
instance_methods.each(&hook.(cls))
|
74
|
+
class_methods.each(&hook.(cls.singleton_class))
|
125
75
|
end
|
126
76
|
|
127
77
|
tp.enable(&block)
|
@@ -0,0 +1,78 @@
|
|
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
|
+
def initialize(hook_class, hook_method)
|
10
|
+
@hook_class = hook_class
|
11
|
+
@hook_method = hook_method
|
12
|
+
@defined_class, method_symbol = Hook.qualify_method_name(@hook_method)
|
13
|
+
@method_display_name = [@defined_class, method_symbol, @hook_method.name].join
|
14
|
+
end
|
15
|
+
|
16
|
+
def activate
|
17
|
+
warn "AppMap: Hooking #{method_display_name}" if Hook::LOG
|
18
|
+
|
19
|
+
hook_method = self.hook_method
|
20
|
+
before_hook = self.method(:before_hook)
|
21
|
+
after_hook = self.method(:after_hook)
|
22
|
+
with_disabled_hook = self.method(:with_disabled_hook)
|
23
|
+
|
24
|
+
hook_class.define_method hook_method.name do |*args, &block|
|
25
|
+
instance_method = hook_method.bind(self).to_proc
|
26
|
+
|
27
|
+
hook_disabled = Thread.current[HOOK_DISABLE_KEY]
|
28
|
+
enabled = true if !hook_disabled && AppMap.tracing.enabled?
|
29
|
+
return instance_method.call(*args, &block) unless enabled
|
30
|
+
|
31
|
+
call_event, start_time = with_disabled_hook.() do
|
32
|
+
before_hook.(self, args)
|
33
|
+
end
|
34
|
+
return_value = nil
|
35
|
+
exception = nil
|
36
|
+
begin
|
37
|
+
return_value = instance_method.(*args, &block)
|
38
|
+
rescue
|
39
|
+
exception = $ERROR_INFO
|
40
|
+
raise
|
41
|
+
ensure
|
42
|
+
with_disabled_hook.() do
|
43
|
+
after_hook.(call_event, start_time, return_value, exception)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
protected
|
50
|
+
|
51
|
+
def before_hook(receiver, args)
|
52
|
+
require 'appmap/event'
|
53
|
+
call_event = AppMap::Event::MethodCall.build_from_invocation(defined_class, hook_method, receiver, args)
|
54
|
+
AppMap.tracing.record_event call_event, defined_class: defined_class, method: hook_method
|
55
|
+
[ call_event, Time.now ]
|
56
|
+
end
|
57
|
+
|
58
|
+
def after_hook(call_event, start_time, return_value, exception)
|
59
|
+
require 'appmap/event'
|
60
|
+
elapsed = Time.now - start_time
|
61
|
+
return_event = \
|
62
|
+
AppMap::Event::MethodReturn.build_from_invocation call_event.id, elapsed, return_value, exception
|
63
|
+
AppMap.tracing.record_event return_event
|
64
|
+
end
|
65
|
+
|
66
|
+
def with_disabled_hook(&fn)
|
67
|
+
# Don't record functions, such as to_s and inspect, that might be called
|
68
|
+
# by the fn. Otherwise there can be a stack overflow.
|
69
|
+
Thread.current[HOOK_DISABLE_KEY] = true
|
70
|
+
begin
|
71
|
+
fn.call
|
72
|
+
ensure
|
73
|
+
Thread.current[HOOK_DISABLE_KEY] = false
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -16,11 +16,11 @@ module AppMap
|
|
16
16
|
class HTTPServerRequest
|
17
17
|
include ContextKey
|
18
18
|
|
19
|
-
class Call < AppMap::Event::
|
19
|
+
class Call < AppMap::Event::MethodCall
|
20
20
|
attr_accessor :payload
|
21
21
|
|
22
|
-
def initialize(
|
23
|
-
super AppMap::Event.next_id_counter, :call,
|
22
|
+
def initialize(payload)
|
23
|
+
super AppMap::Event.next_id_counter, :call, Thread.current.object_id
|
24
24
|
|
25
25
|
self.payload = payload
|
26
26
|
end
|
@@ -47,7 +47,7 @@ module AppMap
|
|
47
47
|
end
|
48
48
|
|
49
49
|
def call(_, started, finished, _, payload) # (name, started, finished, unique_id, payload)
|
50
|
-
event = Call.new(
|
50
|
+
event = Call.new(payload)
|
51
51
|
Thread.current[context_key] = Context.new(event.id, Time.now)
|
52
52
|
AppMap.tracing.record_event(event)
|
53
53
|
end
|
@@ -59,8 +59,8 @@ module AppMap
|
|
59
59
|
class Call < AppMap::Event::MethodReturnIgnoreValue
|
60
60
|
attr_accessor :payload
|
61
61
|
|
62
|
-
def initialize(
|
63
|
-
super AppMap::Event.next_id_counter, :return,
|
62
|
+
def initialize(payload, parent_id, elapsed)
|
63
|
+
super AppMap::Event.next_id_counter, :return, Thread.current.object_id
|
64
64
|
|
65
65
|
self.payload = payload
|
66
66
|
self.parent_id = parent_id
|
@@ -82,7 +82,7 @@ module AppMap
|
|
82
82
|
context = Thread.current[context_key]
|
83
83
|
Thread.current[context_key] = nil
|
84
84
|
|
85
|
-
event = Call.new(
|
85
|
+
event = Call.new(payload, context.id, Time.now - context.start_time)
|
86
86
|
AppMap.tracing.record_event(event)
|
87
87
|
end
|
88
88
|
end
|
@@ -5,11 +5,11 @@ require 'appmap/event'
|
|
5
5
|
module AppMap
|
6
6
|
module Rails
|
7
7
|
class SQLHandler
|
8
|
-
class SQLCall < AppMap::Event::
|
8
|
+
class SQLCall < AppMap::Event::MethodCall
|
9
9
|
attr_accessor :payload
|
10
10
|
|
11
|
-
def initialize(
|
12
|
-
super AppMap::Event.next_id_counter, :call,
|
11
|
+
def initialize(payload)
|
12
|
+
super AppMap::Event.next_id_counter, :call, Thread.current.object_id
|
13
13
|
|
14
14
|
self.payload = payload
|
15
15
|
end
|
@@ -20,7 +20,7 @@ module AppMap
|
|
20
20
|
sql: payload[:sql],
|
21
21
|
database_type: payload[:database_type]
|
22
22
|
}.tap do |sql_query|
|
23
|
-
%i[server_version
|
23
|
+
%i[server_version].each do |attribute|
|
24
24
|
sql_query[attribute] = payload[attribute] if payload[attribute]
|
25
25
|
end
|
26
26
|
end
|
@@ -29,8 +29,8 @@ module AppMap
|
|
29
29
|
end
|
30
30
|
|
31
31
|
class SQLReturn < AppMap::Event::MethodReturnIgnoreValue
|
32
|
-
def initialize(
|
33
|
-
super AppMap::Event.next_id_counter, :return,
|
32
|
+
def initialize(parent_id, elapsed)
|
33
|
+
super AppMap::Event.next_id_counter, :return, Thread.current.object_id
|
34
34
|
|
35
35
|
self.parent_id = parent_id
|
36
36
|
self.elapsed = elapsed
|
@@ -76,6 +76,8 @@ module AppMap
|
|
76
76
|
case database_type
|
77
77
|
when :postgres
|
78
78
|
ActiveRecord::Base.connection.postgresql_version
|
79
|
+
when :sqlite
|
80
|
+
ActiveRecord::Base.connection.database_version.to_s
|
79
81
|
else
|
80
82
|
warn "Unable to determine database version for #{database_type.inspect}"
|
81
83
|
end
|
@@ -133,9 +135,9 @@ module AppMap
|
|
133
135
|
|
134
136
|
SQLExaminer.examine payload, sql: sql
|
135
137
|
|
136
|
-
call = SQLCall.new(
|
138
|
+
call = SQLCall.new(payload)
|
137
139
|
AppMap.tracing.record_event(call)
|
138
|
-
AppMap.tracing.record_event(SQLReturn.new(
|
140
|
+
AppMap.tracing.record_event(SQLReturn.new(call.id, finished - started))
|
139
141
|
ensure
|
140
142
|
Thread.current[reentry_key] = nil
|
141
143
|
end
|
data/lib/appmap/trace.rb
CHANGED
@@ -14,34 +14,34 @@ module AppMap
|
|
14
14
|
|
15
15
|
class Tracing
|
16
16
|
def initialize
|
17
|
-
@
|
17
|
+
@tracing = []
|
18
18
|
end
|
19
19
|
|
20
20
|
def empty?
|
21
|
-
@
|
21
|
+
@tracing.empty?
|
22
22
|
end
|
23
23
|
|
24
24
|
def trace(enable: true)
|
25
25
|
Tracer.new.tap do |tracer|
|
26
|
-
@
|
26
|
+
@tracing << tracer
|
27
27
|
tracer.enable if enable
|
28
28
|
end
|
29
29
|
end
|
30
30
|
|
31
31
|
def enabled?
|
32
|
-
@
|
32
|
+
@tracing.any?(&:enabled?)
|
33
33
|
end
|
34
34
|
|
35
35
|
def record_event(event, defined_class: nil, method: nil)
|
36
|
-
@
|
36
|
+
@tracing.each do |tracer|
|
37
37
|
tracer.record_event(event, defined_class: defined_class, method: method)
|
38
38
|
end
|
39
39
|
end
|
40
40
|
|
41
41
|
def delete(tracer)
|
42
|
-
return unless @
|
42
|
+
return unless @tracing.member?(tracer)
|
43
43
|
|
44
|
-
@
|
44
|
+
@tracing.delete(tracer)
|
45
45
|
tracer.disable
|
46
46
|
end
|
47
47
|
end
|
data/lib/appmap/util.rb
CHANGED
@@ -35,6 +35,25 @@ module AppMap
|
|
35
35
|
|
36
36
|
[ fname, extension ].join
|
37
37
|
end
|
38
|
+
|
39
|
+
# sanitize_event removes ephemeral values from an event, making
|
40
|
+
# events easier to compare across runs.
|
41
|
+
def sanitize_event(event, &block)
|
42
|
+
event.delete(:thread_id)
|
43
|
+
event.delete(:elapsed)
|
44
|
+
delete_object_id = ->(obj) { (obj || {}).delete(:object_id) }
|
45
|
+
delete_object_id.call(event[:receiver])
|
46
|
+
delete_object_id.call(event[:return_value])
|
47
|
+
(event[:parameters] || []).each(&delete_object_id)
|
48
|
+
(event[:exceptions] || []).each(&delete_object_id)
|
49
|
+
|
50
|
+
case event[:event]
|
51
|
+
when :call
|
52
|
+
event[:path] = event[:path].gsub(Gem.dir + '/', '')
|
53
|
+
end
|
54
|
+
|
55
|
+
event
|
56
|
+
end
|
38
57
|
end
|
39
58
|
end
|
40
59
|
end
|
data/lib/appmap/version.rb
CHANGED
@@ -48,11 +48,11 @@ describe 'AbstractControllerBase' do
|
|
48
48
|
|
49
49
|
expect(appmap).to match(<<-CREATE_CALL.strip)
|
50
50
|
event: call
|
51
|
+
thread_id: .*
|
51
52
|
defined_class: Api::UsersController
|
52
53
|
method_id: build_user
|
53
54
|
path: app/controllers/api/users_controller.rb
|
54
55
|
lineno: 23
|
55
|
-
thread_id: .*
|
56
56
|
static: false
|
57
57
|
parameters:
|
58
58
|
- name: params
|
@@ -47,17 +47,17 @@ describe 'AbstractControllerBase' do
|
|
47
47
|
SERVER_REQUEST
|
48
48
|
end
|
49
49
|
|
50
|
-
it '
|
50
|
+
it 'properly captures method parameters in the appmap' do
|
51
51
|
expect(File).to exist(appmap_json)
|
52
52
|
appmap = JSON.parse(File.read(appmap_json)).to_yaml
|
53
53
|
|
54
54
|
expect(appmap).to match(<<-CREATE_CALL.strip)
|
55
55
|
event: call
|
56
|
+
thread_id: .*
|
56
57
|
defined_class: Api::UsersController
|
57
58
|
method_id: build_user
|
58
59
|
path: app/controllers/api/users_controller.rb
|
59
60
|
lineno: 23
|
60
|
-
thread_id: .*
|
61
61
|
static: false
|
62
62
|
parameters:
|
63
63
|
- name: params
|
@@ -68,5 +68,12 @@ describe 'AbstractControllerBase' do
|
|
68
68
|
receiver:
|
69
69
|
CREATE_CALL
|
70
70
|
end
|
71
|
+
|
72
|
+
it 'returns a minimal event' do
|
73
|
+
expect(File).to exist(appmap_json)
|
74
|
+
appmap = JSON.parse(File.read(appmap_json))
|
75
|
+
event = appmap['events'].find { |event| event['event'] == 'return' && event['return_value'] }
|
76
|
+
expect(event.keys).to eq(%w[id event thread_id parent_id elapsed return_value])
|
77
|
+
end
|
71
78
|
end
|
72
79
|
end
|
data/spec/hook_spec.rb
CHANGED
@@ -16,36 +16,18 @@ end
|
|
16
16
|
Psych::Visitors::YAMLTree.prepend(ShowYamlNulls)
|
17
17
|
|
18
18
|
describe 'AppMap class Hooking', docker: false do
|
19
|
+
require 'appmap/util'
|
19
20
|
def collect_events(tracer)
|
20
21
|
[].tap do |events|
|
21
22
|
while tracer.event?
|
22
23
|
events << tracer.next_event.to_h
|
23
24
|
end
|
24
|
-
end.map
|
25
|
-
event.delete(:thread_id)
|
26
|
-
event.delete(:elapsed)
|
27
|
-
delete_object_id = ->(obj) { (obj || {}).delete(:object_id) }
|
28
|
-
delete_object_id.call(event[:receiver])
|
29
|
-
delete_object_id.call(event[:return_value])
|
30
|
-
(event[:parameters] || []).each(&delete_object_id)
|
31
|
-
(event[:exceptions] || []).each(&delete_object_id)
|
32
|
-
|
33
|
-
case event[:event]
|
34
|
-
when :call
|
35
|
-
event[:path] = event[:path].gsub(Gem.dir + '/', '')
|
36
|
-
when :return
|
37
|
-
# These should be removed from the appmap spec
|
38
|
-
%i[defined_class method_id path lineno static].each do |obsolete_field|
|
39
|
-
event.delete(obsolete_field)
|
40
|
-
end
|
41
|
-
end
|
42
|
-
event
|
43
|
-
end.to_yaml
|
25
|
+
end.map(&AppMap::Util.method(:sanitize_event)).to_yaml
|
44
26
|
end
|
45
27
|
|
46
28
|
def invoke_test_file(file, setup: nil, &block)
|
47
29
|
AppMap.configuration = nil
|
48
|
-
package = AppMap::Package.new(file, [])
|
30
|
+
package = AppMap::Package.new(file, nil, [])
|
49
31
|
config = AppMap::Config.new('hook_spec', [ package ])
|
50
32
|
AppMap.configuration = config
|
51
33
|
tracer = nil
|
@@ -99,7 +81,7 @@ describe 'AppMap class Hooking', docker: false do
|
|
99
81
|
:class: String
|
100
82
|
:value: default
|
101
83
|
YAML
|
102
|
-
|
84
|
+
test_hook_behavior 'spec/fixtures/hook/instance_method.rb', events_yaml do
|
103
85
|
expect(InstanceMethod.new.say_default).to eq('default')
|
104
86
|
end
|
105
87
|
end
|
@@ -456,20 +438,6 @@ describe 'AppMap class Hooking', docker: false do
|
|
456
438
|
end
|
457
439
|
end
|
458
440
|
|
459
|
-
context 'OpenSSL::X509::Certificate.sign' do
|
460
|
-
# OpenSSL::X509 is not being hooked.
|
461
|
-
# This might be because the class is being loaded before AppMap, and so the TracePoint
|
462
|
-
# set by AppMap doesn't see it.
|
463
|
-
xit 'is hooked' do
|
464
|
-
events_yaml = <<~YAML
|
465
|
-
---
|
466
|
-
YAML
|
467
|
-
test_hook_behavior 'spec/fixtures/hook/openssl_sign.rb', events_yaml do
|
468
|
-
expect(OpenSSLExample.example).to be_truthy
|
469
|
-
end
|
470
|
-
end
|
471
|
-
end
|
472
|
-
|
473
441
|
context 'ActiveSupport::SecurityUtils.secure_compare' do
|
474
442
|
it 'is hooked' do
|
475
443
|
events_yaml = <<~YAML
|
@@ -560,7 +528,7 @@ describe 'AppMap class Hooking', docker: false do
|
|
560
528
|
:labels:
|
561
529
|
- security
|
562
530
|
YAML
|
563
|
-
|
531
|
+
|
564
532
|
config, tracer = invoke_test_file 'spec/fixtures/hook/compare.rb' do
|
565
533
|
expect(Compare.compare('string', 'string')).to be_truthy
|
566
534
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'rails_spec_helper'
|
2
2
|
|
3
|
-
describe '
|
3
|
+
describe 'SQL events' do
|
4
4
|
before(:all) { @fixture_dir = 'spec/fixtures/rails_users_app' }
|
5
5
|
include_context 'Rails app pg database'
|
6
6
|
|
@@ -14,54 +14,77 @@ describe 'Record SQL queries in a Rails app' do
|
|
14
14
|
end
|
15
15
|
|
16
16
|
let(:tmpdir) { "tmp/spec/record_sql_rails_pg_spec" }
|
17
|
-
let(:appmap) { JSON.parse(File.read(appmap_json)).to_yaml }
|
18
17
|
|
19
|
-
|
18
|
+
describe 'fields' do
|
20
19
|
let(:test_line_number) { 8 }
|
21
20
|
let(:appmap_json) { File.join(tmpdir, 'appmap/rspec/Api_UsersController_POST_api_users_with_required_parameters_creates_a_user.appmap.json') }
|
21
|
+
let(:orm_module) { 'sequel' }
|
22
|
+
let(:appmap) { JSON.parse(File.read(appmap_json)) }
|
23
|
+
describe 'on a call event' do
|
24
|
+
let(:event) do
|
25
|
+
appmap['events'].find do |event|
|
26
|
+
event['event'] == 'call' &&
|
27
|
+
event.keys.include?('sql_query')
|
28
|
+
end
|
29
|
+
end
|
30
|
+
it 'do not include function-only fields' do
|
31
|
+
expect(event.keys).to_not include('defined_class')
|
32
|
+
expect(event.keys).to_not include('method_id')
|
33
|
+
expect(event.keys).to_not include('path')
|
34
|
+
expect(event.keys).to_not include('lineno')
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe 'in a Rails app' do
|
40
|
+
let(:appmap) { JSON.parse(File.read(appmap_json)).to_yaml }
|
41
|
+
context 'while creating a new record' do
|
42
|
+
let(:test_line_number) { 8 }
|
43
|
+
let(:appmap_json) { File.join(tmpdir, 'appmap/rspec/Api_UsersController_POST_api_users_with_required_parameters_creates_a_user.appmap.json') }
|
22
44
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
45
|
+
context 'using Sequel ORM' do
|
46
|
+
let(:orm_module) { 'sequel' }
|
47
|
+
it 'detects the sql INSERT' do
|
48
|
+
expect(appmap).to include(<<-SQL_QUERY.strip)
|
27
49
|
sql_query:
|
28
50
|
sql: INSERT INTO "users" ("login") VALUES ('alice') RETURNING *
|
29
|
-
|
51
|
+
SQL_QUERY
|
52
|
+
end
|
30
53
|
end
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
expect(appmap).to include(<<-SQL_QUERY.strip)
|
54
|
+
context 'using ActiveRecord ORM' do
|
55
|
+
let(:orm_module) { 'activerecord' }
|
56
|
+
it 'detects the sql INSERT' do
|
57
|
+
expect(appmap).to include(<<-SQL_QUERY.strip)
|
36
58
|
sql_query:
|
37
59
|
sql: INSERT INTO "users" ("login") VALUES ($1) RETURNING "id"
|
38
|
-
|
60
|
+
SQL_QUERY
|
61
|
+
end
|
39
62
|
end
|
40
63
|
end
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
expect(appmap).to include(<<-SQL_QUERY.strip)
|
64
|
+
|
65
|
+
context 'while listing records' do
|
66
|
+
let(:test_line_number) { 23 }
|
67
|
+
let(:appmap_json) { File.join(tmpdir, 'appmap/rspec/Api_UsersController_GET_api_users_lists_the_users.appmap.json') }
|
68
|
+
|
69
|
+
context 'using Sequel ORM' do
|
70
|
+
let(:orm_module) { 'sequel' }
|
71
|
+
it 'detects the sql SELECT' do
|
72
|
+
expect(appmap).to include(<<-SQL_QUERY.strip)
|
51
73
|
sql_query:
|
52
74
|
sql: SELECT * FROM "users"
|
53
|
-
|
54
|
-
|
55
|
-
|
75
|
+
SQL_QUERY
|
76
|
+
|
77
|
+
expect(appmap).to include('sql:')
|
78
|
+
end
|
56
79
|
end
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
expect(appmap).to include(<<-SQL_QUERY.strip)
|
80
|
+
context 'using ActiveRecord ORM' do
|
81
|
+
let(:orm_module) { 'activerecord' }
|
82
|
+
it 'detects the sql SELECT' do
|
83
|
+
expect(appmap).to include(<<-SQL_QUERY.strip)
|
62
84
|
sql_query:
|
63
85
|
sql: SELECT "users".* FROM "users"
|
64
|
-
|
86
|
+
SQL_QUERY
|
87
|
+
end
|
65
88
|
end
|
66
89
|
end
|
67
90
|
end
|
data/test/cli_test.rb
CHANGED
@@ -55,7 +55,7 @@ class CLITest < Minitest::Test
|
|
55
55
|
assert_equal <<~OUTPUT.strip, output.strip
|
56
56
|
Class frequency:
|
57
57
|
----------------
|
58
|
-
|
58
|
+
1 Main
|
59
59
|
|
60
60
|
Method frequency:
|
61
61
|
----------------
|
@@ -79,7 +79,7 @@ class CLITest < Minitest::Test
|
|
79
79
|
"class_frequency": [
|
80
80
|
{
|
81
81
|
"name": "Main",
|
82
|
-
"count":
|
82
|
+
"count": 1
|
83
83
|
}
|
84
84
|
],
|
85
85
|
"method_frequency": [
|
data/test/test_helper.rb
CHANGED
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.
|
4
|
+
version: 0.32.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kevin Gilpin
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-08-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -273,6 +273,7 @@ files:
|
|
273
273
|
- lib/appmap/cucumber.rb
|
274
274
|
- lib/appmap/event.rb
|
275
275
|
- lib/appmap/hook.rb
|
276
|
+
- lib/appmap/hook/method.rb
|
276
277
|
- lib/appmap/metadata.rb
|
277
278
|
- lib/appmap/middleware/remote_recording.rb
|
278
279
|
- lib/appmap/minitest.rb
|
@@ -311,7 +312,6 @@ files:
|
|
311
312
|
- spec/fixtures/hook/constructor.rb
|
312
313
|
- spec/fixtures/hook/exception_method.rb
|
313
314
|
- spec/fixtures/hook/instance_method.rb
|
314
|
-
- spec/fixtures/hook/openssl_sign.rb
|
315
315
|
- spec/fixtures/hook/singleton_method.rb
|
316
316
|
- spec/fixtures/rack_users_app/.dockerignore
|
317
317
|
- spec/fixtures/rack_users_app/.gitignore
|
@@ -1,87 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
# From the manual page https://ruby-doc.org/stdlib-2.5.1/libdoc/openssl/rdoc/OpenSSL.html
|
4
|
-
|
5
|
-
require 'openssl'
|
6
|
-
|
7
|
-
module OpenSSLExample
|
8
|
-
def OpenSSLExample.example
|
9
|
-
ca_key = OpenSSL::PKey::RSA.new 2048
|
10
|
-
pass_phrase = 'my secure pass phrase goes here'
|
11
|
-
|
12
|
-
cipher = OpenSSL::Cipher.new 'AES-256-CBC'
|
13
|
-
|
14
|
-
open 'tmp/ca_key.pem', 'w', 0644 do |io|
|
15
|
-
io.write ca_key.export(cipher, pass_phrase)
|
16
|
-
end
|
17
|
-
|
18
|
-
ca_name = OpenSSL::X509::Name.parse '/CN=ca/DC=example'
|
19
|
-
|
20
|
-
ca_cert = OpenSSL::X509::Certificate.new
|
21
|
-
ca_cert.serial = 0
|
22
|
-
ca_cert.version = 2
|
23
|
-
ca_cert.not_before = Time.now
|
24
|
-
ca_cert.not_after = Time.now + 86400
|
25
|
-
|
26
|
-
ca_cert.public_key = ca_key.public_key
|
27
|
-
ca_cert.subject = ca_name
|
28
|
-
ca_cert.issuer = ca_name
|
29
|
-
|
30
|
-
extension_factory = OpenSSL::X509::ExtensionFactory.new
|
31
|
-
extension_factory.subject_certificate = ca_cert
|
32
|
-
extension_factory.issuer_certificate = ca_cert
|
33
|
-
|
34
|
-
ca_cert.add_extension extension_factory.create_extension('subjectKeyIdentifier', 'hash')
|
35
|
-
ca_cert.add_extension extension_factory.create_extension('basicConstraints', 'CA:TRUE', true)
|
36
|
-
|
37
|
-
ca_cert.add_extension extension_factory.create_extension(
|
38
|
-
'keyUsage', 'cRLSign,keyCertSign', true)
|
39
|
-
|
40
|
-
ca_cert.sign ca_key, OpenSSL::Digest::SHA1.new
|
41
|
-
|
42
|
-
open 'tmp/ca_cert.pem', 'w' do |io|
|
43
|
-
io.write ca_cert.to_pem
|
44
|
-
end
|
45
|
-
|
46
|
-
csr = OpenSSL::X509::Request.new
|
47
|
-
csr.version = 0
|
48
|
-
csr.subject = OpenSSL::X509::Name.new([ ['CN', 'the name to sign', OpenSSL::ASN1::UTF8STRING] ])
|
49
|
-
csr.public_key = ca_key.public_key
|
50
|
-
csr.sign ca_key, OpenSSL::Digest::SHA1.new
|
51
|
-
|
52
|
-
open 'tmp/csr.pem', 'w' do |io|
|
53
|
-
io.write csr.to_pem
|
54
|
-
end
|
55
|
-
|
56
|
-
csr = OpenSSL::X509::Request.new File.read 'tmp/csr.pem'
|
57
|
-
|
58
|
-
raise 'CSR can not be verified' unless csr.verify csr.public_key
|
59
|
-
|
60
|
-
csr_cert = OpenSSL::X509::Certificate.new
|
61
|
-
csr_cert.serial = 0
|
62
|
-
csr_cert.version = 2
|
63
|
-
csr_cert.not_before = Time.now
|
64
|
-
csr_cert.not_after = Time.now + 600
|
65
|
-
|
66
|
-
csr_cert.subject = csr.subject
|
67
|
-
csr_cert.public_key = csr.public_key
|
68
|
-
csr_cert.issuer = ca_cert.subject
|
69
|
-
|
70
|
-
extension_factory = OpenSSL::X509::ExtensionFactory.new
|
71
|
-
extension_factory.subject_certificate = csr_cert
|
72
|
-
extension_factory.issuer_certificate = ca_cert
|
73
|
-
|
74
|
-
csr_cert.add_extension extension_factory.create_extension('basicConstraints', 'CA:FALSE')
|
75
|
-
|
76
|
-
csr_cert.add_extension extension_factory.create_extension(
|
77
|
-
'keyUsage', 'keyEncipherment,dataEncipherment,digitalSignature')
|
78
|
-
|
79
|
-
csr_cert.add_extension extension_factory.create_extension('subjectKeyIdentifier', 'hash')
|
80
|
-
|
81
|
-
csr_cert.sign ca_key, OpenSSL::Digest::SHA1.new
|
82
|
-
|
83
|
-
open 'tmp/csr_cert.pem', 'w' do |io|
|
84
|
-
io.write csr_cert.to_pem
|
85
|
-
end
|
86
|
-
end
|
87
|
-
end
|