ruby-production-breakpoints 0.0.5 → 0.0.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/ruby-production-breakpoints/breakpoints/base.rb +72 -0
- data/lib/ruby-production-breakpoints/breakpoints/inspect.rb +19 -0
- data/lib/ruby-production-breakpoints/breakpoints/latency.rb +20 -0
- data/lib/ruby-production-breakpoints/breakpoints/locals.rb +21 -0
- data/lib/ruby-production-breakpoints/breakpoints/ustack.rb +74 -0
- data/lib/ruby-production-breakpoints/breakpoints.rb +7 -0
- data/lib/ruby-production-breakpoints/configuration.rb +50 -0
- data/lib/ruby-production-breakpoints/errors.rb +5 -0
- data/lib/ruby-production-breakpoints/method_override.rb +30 -0
- data/lib/ruby-production-breakpoints/parser.rb +140 -0
- data/lib/ruby-production-breakpoints/platform.rb +23 -0
- data/lib/ruby-production-breakpoints/start_end_line_validator.rb +15 -0
- data/lib/ruby-production-breakpoints/version.rb +6 -0
- data/lib/ruby-production-breakpoints.rb +110 -0
- metadata +16 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c667c558395ef7da2130c3085956a438366a9ae49c9531b97ab08f994a90e157
|
4
|
+
data.tar.gz: 946c38475edf95386ac832e82815e5a5cb0efeb1a7f1d9a05300939dbc6fab94
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0146b4b990c9a9ac018eb4b3dc365891cfb9b55b0b8b9fc902ebfb4bb9ab50eeb548e3536d9a02d5ffd5568eb5c47dfb2d735f5c6339ee0153ae1f3412cf54c8
|
7
|
+
data.tar.gz: 4ed8781d90ea4351d1ff980726aac868198e46bbadcb820c88de0b6057464d8a255f9ba9a010919049e6ab8c746e0274dfb5a85828d93f3c9f1d0100a9681c05
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'unmixer'
|
4
|
+
using Unmixer
|
5
|
+
|
6
|
+
module ProductionBreakpoints
|
7
|
+
module Breakpoints
|
8
|
+
class Base
|
9
|
+
TRACEPOINT_TYPES = [].freeze
|
10
|
+
|
11
|
+
attr_reader :provider_name, :name, :tracepoint
|
12
|
+
|
13
|
+
def initialize(source_file, start_line, end_line, trace_id: 1)
|
14
|
+
@injector_module = nil
|
15
|
+
@source_file = source_file
|
16
|
+
@start_line = start_line
|
17
|
+
@end_line = end_line
|
18
|
+
@trace_id = trace_id
|
19
|
+
@method = self.class.name.split('::').last.downcase
|
20
|
+
@parser = ProductionBreakpoints::Parser.new(@source_file)
|
21
|
+
@node = @parser.find_definition_node(@start_line, @end_line)
|
22
|
+
@method_override = ProductionBreakpoints::MethodOverride.new(@parser, start_line, end_line)
|
23
|
+
@ns = Object.const_get(@parser.find_definition_namespace(@node)) # FIXME: error handling, if not found
|
24
|
+
@provider_name = File.basename(@source_file).gsub('.', '_')
|
25
|
+
@name = "#{@method}_#{@trace_id}"
|
26
|
+
@tracepoint = StaticTracing::Tracepoint.new(@provider_name, @name, *self.class.const_get('TRACEPOINT_TYPES'))
|
27
|
+
end
|
28
|
+
|
29
|
+
def install
|
30
|
+
@injector_module = build_redefined_definition_module(@node)
|
31
|
+
@ns.prepend(@injector_module)
|
32
|
+
end
|
33
|
+
|
34
|
+
# FIXME: saftey if already uninstalled
|
35
|
+
def uninstall
|
36
|
+
@ns.instance_eval { unprepend(@injector_module) }
|
37
|
+
@injector_module = nil
|
38
|
+
end
|
39
|
+
|
40
|
+
def load
|
41
|
+
@tracepoint.provider.enable
|
42
|
+
end
|
43
|
+
|
44
|
+
def unload
|
45
|
+
@tracepoint.provider.disable
|
46
|
+
end
|
47
|
+
|
48
|
+
# Allows for specific handling of the selected lines
|
49
|
+
def handle(caller_binding)
|
50
|
+
eval(@method_override.handler_src.join, caller_binding)
|
51
|
+
end
|
52
|
+
|
53
|
+
def resume(caller_binding)
|
54
|
+
eval(@method_override.resume_src.join, caller_binding) if @method_override.resume_src
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
# A custom module we'll prepend in order to override
|
60
|
+
# us to keep the expected binding for the wrapped code, and remainder of the method
|
61
|
+
def build_redefined_definition_module(node)
|
62
|
+
# This is the metaprogramming to inject our breakpoint handle
|
63
|
+
handler = "ProductionBreakpoints.installed_breakpoints[:#{@trace_id}].handle(Kernel.binding)"
|
64
|
+
|
65
|
+
# This injects our handler at the end of the original source code
|
66
|
+
injected = @parser.inject_metaprogramming_handlers(handler, node.first_lineno, node.last_lineno)
|
67
|
+
# ProductionBreakpoints.config.logger.debug(injected)
|
68
|
+
Module.new { module_eval { eval(injected); eval('def production_breakpoint_enabled?; true; end;') } }
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ProductionBreakpoints
|
4
|
+
module Breakpoints
|
5
|
+
# Inspect result of the last evaluated expression
|
6
|
+
class Inspect < Base
|
7
|
+
TRACEPOINT_TYPES = [String].freeze
|
8
|
+
|
9
|
+
def handle(caller_binding, &block)
|
10
|
+
return super(caller_binding, &block) unless @tracepoint.enabled?
|
11
|
+
|
12
|
+
val = super(caller_binding, &block)
|
13
|
+
@tracepoint.fire(val.inspect)
|
14
|
+
|
15
|
+
resume(caller_binding, &block) || val
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ProductionBreakpoints
|
4
|
+
module Breakpoints
|
5
|
+
# Exposes nanosecond the latency of executing the selected lines
|
6
|
+
class Latency < Base # FIXME: refactor a bunch of these idioms into Base
|
7
|
+
TRACEPOINT_TYPES = [Integer].freeze
|
8
|
+
|
9
|
+
def handle(caller_binding, &block)
|
10
|
+
return super(caller_binding, &block) unless @tracepoint.enabled?
|
11
|
+
|
12
|
+
start_time = StaticTracing.nsec
|
13
|
+
val = super(caller_binding, &block)
|
14
|
+
duration = StaticTracing.nsec - start_time
|
15
|
+
@tracepoint.fire(duration)
|
16
|
+
resume(caller_binding, &block) || val
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ProductionBreakpoints
|
4
|
+
module Breakpoints
|
5
|
+
# Show local variables and their values
|
6
|
+
class Locals < Base # FIXME: refactor a bunch of these idioms into Base
|
7
|
+
TRACEPOINT_TYPES = [String].freeze
|
8
|
+
|
9
|
+
def handle(caller_binding, &block)
|
10
|
+
return super(caller_binding, &block) unless @tracepoint.enabled?
|
11
|
+
|
12
|
+
val = super(caller_binding, &block)
|
13
|
+
locals = caller_binding.local_variables
|
14
|
+
locals.delete(:local_bind)
|
15
|
+
vals = locals.map { |v| [v, caller_binding.local_variable_get(v)] }.to_h
|
16
|
+
@tracepoint.fire(vals.to_json)
|
17
|
+
resume(caller_binding, &block) || val
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# # frozen_string_literal: true
|
2
|
+
|
3
|
+
# module ProductionBreakpoints
|
4
|
+
# module Breakpoints
|
5
|
+
# # Show local variables and their values
|
6
|
+
# class Ustack < Base
|
7
|
+
# TRACEPOINT_TYPES = [String, String, String,
|
8
|
+
# String, String, String].freeze
|
9
|
+
# MAX_STACK_STR_SIZE = MAX_USDT_STR_SIZE * TRACEPOINT_TYPES.size
|
10
|
+
|
11
|
+
# def initialize(*args, &block)
|
12
|
+
# super(*args, &block)
|
13
|
+
# get_ruby_stack_str = <<-EOS
|
14
|
+
# caller.map { |l| l.split(":in ") }.to_h
|
15
|
+
# EOS
|
16
|
+
# @handler_iseq = RubyVM::InstructionSequence.compile(get_ruby_stack_str)
|
17
|
+
# end
|
18
|
+
|
19
|
+
# def handle(vm_tracepoint)
|
20
|
+
# return unless @tracepoint.enabled?
|
21
|
+
|
22
|
+
# stack_map = @handler_iseq.eval(vm_tracepoint.binding)
|
23
|
+
|
24
|
+
# shortened_map = {}
|
25
|
+
# stack_map.each do |k,v|
|
26
|
+
# newkey = k
|
27
|
+
# #if k.include?('gems')
|
28
|
+
# # newkey = k[k.rindex('gems')..-1].split(File::SEPARATOR)[1..-1]
|
29
|
+
# # .join(File::SEPARATOR)
|
30
|
+
# #elsif k.include?('lib')
|
31
|
+
# # newkey = k[k.rindex('lib')..-1].split(File::SEPARATOR)[1..-1]
|
32
|
+
# # .join(File::SEPARATOR)
|
33
|
+
# #end
|
34
|
+
# # FIXME lots of context is lost this way, as the above methods
|
35
|
+
# # consistently exceed the max size.
|
36
|
+
# # Need to find a more optimal way to shorten the stack here, to
|
37
|
+
# # pack it into the 1200 bytes available
|
38
|
+
# newkey = File.basename(k)
|
39
|
+
# shortened_map[newkey] = v
|
40
|
+
# end
|
41
|
+
# # ProductionBreakpoints.logger.debug(shortened_map.inspect)
|
42
|
+
|
43
|
+
# stack_str = shortened_map.to_json
|
44
|
+
|
45
|
+
# if stack_str.size > (MAX_STACK_STR_SIZE)
|
46
|
+
# ProductionBreakpoints.logger.error("Stack exceeds #{MAX_STACK_STR_SIZE}")
|
47
|
+
# # Truncate because i'm lazy
|
48
|
+
# stack_str = stack_str[0..MAX_STACK_STR_SIZE]
|
49
|
+
# end
|
50
|
+
|
51
|
+
# slices = stack_str.chars.each_slice(MAX_USDT_STR_SIZE).map(&:join)
|
52
|
+
|
53
|
+
# case slices.size
|
54
|
+
|
55
|
+
# when 1
|
56
|
+
# @tracepoint.fire(slices[0], "", "", "", "", "")
|
57
|
+
# when 2
|
58
|
+
# @tracepoint.fire(slices[0], slices[1], "", "", "", "")
|
59
|
+
# when 3
|
60
|
+
# @tracepoint.fire(slices[0], slices[1], slices[2], "", "", "")
|
61
|
+
# when 4
|
62
|
+
# @tracepoint.fire(slices[0], slices[1], slices[2], slices[3], "", "")
|
63
|
+
# when 5
|
64
|
+
# @tracepoint.fire(slices[0], slices[1], slices[2], slices[3],
|
65
|
+
# slices[4], "")
|
66
|
+
# when 6
|
67
|
+
# @tracepoint.fire(slices[0], slices[1], slices[2], slices[3],
|
68
|
+
# slices[4], slices[5])
|
69
|
+
|
70
|
+
# end
|
71
|
+
# end
|
72
|
+
# end
|
73
|
+
# end
|
74
|
+
# end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ruby-production-breakpoints/breakpoints/base'
|
4
|
+
require 'ruby-production-breakpoints/breakpoints/latency'
|
5
|
+
require 'ruby-production-breakpoints/breakpoints/inspect'
|
6
|
+
require 'ruby-production-breakpoints/breakpoints/locals'
|
7
|
+
require 'ruby-production-breakpoints/breakpoints/ustack'
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module ProductionBreakpoints
|
6
|
+
class Configuration
|
7
|
+
# Modes of operation for tracers
|
8
|
+
module Modes
|
9
|
+
ON = 'ON'
|
10
|
+
OFF = 'OFF'
|
11
|
+
SIGNAL = 'SIGNAL'
|
12
|
+
|
13
|
+
module SIGNALS
|
14
|
+
SIGURG = 'URG'
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
attr_reader :mode, :signal, :configured_breakpoints, :logger
|
19
|
+
attr_accessor :path
|
20
|
+
|
21
|
+
# A new configuration instance
|
22
|
+
def initialize
|
23
|
+
@mode = Modes::SIGNAL
|
24
|
+
@signal = Modes::SIGNALS::SIGURG
|
25
|
+
@logger = Logger.new(STDERR)
|
26
|
+
end
|
27
|
+
|
28
|
+
def finish!
|
29
|
+
if File.exist?(path)
|
30
|
+
@configured_breakpoints = JSON.load(File.read(path))
|
31
|
+
enable_trap
|
32
|
+
else
|
33
|
+
logger.error("Config file #{path} not found")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
# Disables trap handler
|
40
|
+
def disable_trap
|
41
|
+
Signal.trap(@signal, 'DEFAULT')
|
42
|
+
end
|
43
|
+
|
44
|
+
# Enables a new trap handler
|
45
|
+
def enable_trap
|
46
|
+
# ProductionBreakpoints.config.logger.debug("trap handler enabled for #{@signal}")
|
47
|
+
Signal.trap(@signal) { ProductionBreakpoints.sync! }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ProductionBreakpoints
|
4
|
+
# Extract valueable parts of the method
|
5
|
+
class MethodOverride
|
6
|
+
def initialize(parser, start_line, end_line)
|
7
|
+
@parser = parser
|
8
|
+
@source_lines = parser.source_lines
|
9
|
+
@node = parser.find_definition_node(start_line, end_line)
|
10
|
+
@start_line = start_line
|
11
|
+
@end_line = end_line
|
12
|
+
end
|
13
|
+
|
14
|
+
def unmodified_src
|
15
|
+
return if @start_line - @node.first_lineno <= 1 # if smaller or equal to one that means we are at the beginning of the method
|
16
|
+
|
17
|
+
@source_lines[(@node.first_lineno)..(@start_line - 2)]
|
18
|
+
end
|
19
|
+
|
20
|
+
def handler_src
|
21
|
+
@source_lines[@start_line - 1..@end_line - 1]
|
22
|
+
end
|
23
|
+
|
24
|
+
def resume_src
|
25
|
+
return if @node.last_lineno - @end_line <= 1 # if smaller or equal to one that means we are at the end of the method
|
26
|
+
|
27
|
+
@source_lines[@end_line..(@node.last_lineno - 2)]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ProductionBreakpoints
|
4
|
+
# FIXME: this class is a mess, figure out interface and properly separate private / public
|
5
|
+
class Parser
|
6
|
+
attr_reader :root_node, :source_lines
|
7
|
+
|
8
|
+
def initialize(source_file)
|
9
|
+
@root_node = RubyVM::AbstractSyntaxTree.parse_file(source_file)
|
10
|
+
@source_lines = File.read(source_file).lines
|
11
|
+
@logger = ProductionBreakpoints.config.logger
|
12
|
+
end
|
13
|
+
|
14
|
+
# FIXME: set a max depth here to pretent unbounded recursion? probably should
|
15
|
+
def find_node(node, type, first, last, depth: 0)
|
16
|
+
child_nodes = node.children.select { |c| c.is_a?(RubyVM::AbstractSyntaxTree::Node) }
|
17
|
+
# @logger.debug("D: #{depth} #{node.type} has #{child_nodes.size} children and spans #{node.first_lineno}:#{node.first_column} to #{node.last_lineno}:#{node.last_column}")
|
18
|
+
|
19
|
+
if node.type == type && first >= node.first_lineno && last <= node.last_lineno
|
20
|
+
return node
|
21
|
+
end
|
22
|
+
|
23
|
+
child_nodes.map { |n| find_node(n, type, first, last, depth: depth + 1) }.flatten
|
24
|
+
end
|
25
|
+
|
26
|
+
def find_lineage(target)
|
27
|
+
lineage = _find_lineage(@root_node, target)
|
28
|
+
lineage.pop # FIXME: verify leafy node is equal to target or throw an error?
|
29
|
+
lineage
|
30
|
+
end
|
31
|
+
|
32
|
+
def find_definition_namespace(target)
|
33
|
+
lineage = find_lineage(target)
|
34
|
+
|
35
|
+
namespaces = []
|
36
|
+
lineage.each do |n|
|
37
|
+
next unless n.type == :MODULE || n.type == :CLASS
|
38
|
+
|
39
|
+
symbols = n.children.select { |c| c.is_a?(RubyVM::AbstractSyntaxTree::Node) && c.type == :COLON2 }
|
40
|
+
if symbols.size != 1
|
41
|
+
@logger.error("Couldn't determine symbol location for parent namespace")
|
42
|
+
end
|
43
|
+
symbol = symbols.first
|
44
|
+
|
45
|
+
symstr = @source_lines[symbol.first_lineno - 1][symbol.first_column..symbol.last_column].strip
|
46
|
+
namespaces << symstr
|
47
|
+
end
|
48
|
+
|
49
|
+
namespaces.join('::')
|
50
|
+
end
|
51
|
+
|
52
|
+
def find_definition_symbol(start_line, end_line)
|
53
|
+
def_node = _find_definition_node(@root_node, start_line, end_line)
|
54
|
+
def_column_start = def_node.first_column
|
55
|
+
def_column_end = _find_args_start(def_node).first_column
|
56
|
+
@source_lines[def_node.first_lineno - 1][(def_column_start + 3 + 1)..def_column_end].strip.to_sym
|
57
|
+
end
|
58
|
+
|
59
|
+
def find_definition_node(start_line, end_line)
|
60
|
+
_find_definition_node(@root_node, start_line, end_line)
|
61
|
+
end
|
62
|
+
|
63
|
+
# This method is a litle weird and pretty deep into metaprogramming, so i'll try to explain it
|
64
|
+
#
|
65
|
+
# Given the source method some_method, and a range of lines to apply the breakpoint to, we will inject
|
66
|
+
# calls two breakpoint methods. We will pass these calls the string representation of the original source code.
|
67
|
+
# If the string of original source is part of the "handle" block, it will run withing the binding
|
68
|
+
# of the method up to that point, and allow for us to run our custom handler method to apply our debugging automation.
|
69
|
+
#
|
70
|
+
# Any remaining code in the method also needs to be eval'd, as we want it to be recognized in the original binding,
|
71
|
+
# and the same binding as we've used for evaluating our handler. This allows us to keep local variables persisted
|
72
|
+
# "between blocks", as we want our breakpoint code to have no impact to the original bindings and source code.
|
73
|
+
#
|
74
|
+
# A generated breakpoint is shown below, the resulting string. is what will be evaluated on the method
|
75
|
+
# that we will prepend to the original parent in order to initiate our override.
|
76
|
+
#
|
77
|
+
# def some_method
|
78
|
+
# a = 1
|
79
|
+
# sleep 0.5
|
80
|
+
# b = a + 1
|
81
|
+
# ProductionBreakpoints.installed_breakpoints[:test_breakpoint_install].handle(Kernel.binding)
|
82
|
+
# end
|
83
|
+
#
|
84
|
+
def inject_metaprogramming_handlers(handler, def_start, def_end)
|
85
|
+
source = @source_lines.dup
|
86
|
+
|
87
|
+
source.insert(def_end - 1, "#{handler}\n") # FIXME: columns? and indenting?
|
88
|
+
source[(def_start - 1)..(def_end)].join
|
89
|
+
end
|
90
|
+
|
91
|
+
def ruby_source(start_line, end_line)
|
92
|
+
@source_lines[(start_line - 1)..(end_line - 1)].join
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def _find_lineage(node, target, depth: 0)
|
98
|
+
child_nodes = node.children.select { |c| c.is_a?(RubyVM::AbstractSyntaxTree::Node) }
|
99
|
+
# @logger.debug("D: #{depth} #{node.type} has #{child_nodes.size} children and spans #{node.first_lineno}:#{node.first_column} to #{node.last_lineno}:#{node.last_column}")
|
100
|
+
|
101
|
+
if node.type == target.type &&
|
102
|
+
|
103
|
+
target.first_lineno >= node.first_lineno &&
|
104
|
+
target.last_lineno <= node.last_lineno
|
105
|
+
return [node]
|
106
|
+
end
|
107
|
+
|
108
|
+
parents = []
|
109
|
+
child_nodes.each do |n|
|
110
|
+
res = _find_lineage(n, target, depth: depth + 1)
|
111
|
+
unless res.empty?
|
112
|
+
res.unshift(n)
|
113
|
+
parents = res
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
parents.flatten
|
118
|
+
end
|
119
|
+
|
120
|
+
# FIXME: better error handling
|
121
|
+
def _find_definition_node(node, start_line, end_line)
|
122
|
+
defs = find_node(node, :DEFN, start_line, end_line)
|
123
|
+
|
124
|
+
if defs.size > 1
|
125
|
+
@logger.error('WHaaat? Multiple definitions found?! Bugs will probably follow')
|
126
|
+
end
|
127
|
+
defs.first
|
128
|
+
end
|
129
|
+
|
130
|
+
# FIXME: better error handling
|
131
|
+
def _find_args_start(def_node)
|
132
|
+
args = find_node(def_node, :ARGS, def_node.first_lineno, def_node.first_lineno)
|
133
|
+
|
134
|
+
if args.size > 1
|
135
|
+
@logger.error("I didn't think this was possible, I must have been wrong")
|
136
|
+
end
|
137
|
+
args.first
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ProductionBreakpoints
|
4
|
+
# Platform detection for ruby-static-tracing
|
5
|
+
module Platform
|
6
|
+
module_function
|
7
|
+
|
8
|
+
# Returns true if platform is linux
|
9
|
+
def linux?
|
10
|
+
/linux/.match(RUBY_PLATFORM)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Returns true if platform is darwin
|
14
|
+
def darwin?
|
15
|
+
/darwin/.match(RUBY_PLATFORM)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Returns true if platform is known to be supported
|
19
|
+
def supported_platform?
|
20
|
+
linux? || darwin?
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ProductionBreakpoints
|
4
|
+
# Validate that start line and end line point to code into the file
|
5
|
+
# At the momemt it will be valid if both start and end line points to valid ruby code
|
6
|
+
module StartEndLineValidator
|
7
|
+
module_function
|
8
|
+
|
9
|
+
def call(source_file, start_line, end_line)
|
10
|
+
source_lines = File.read(source_file).lines
|
11
|
+
|
12
|
+
!source_lines[start_line - 1].strip.empty? && !source_lines[end_line - 1].strip.empty?
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
require 'ruby-static-tracing'
|
6
|
+
|
7
|
+
require 'ruby-production-breakpoints/version'
|
8
|
+
require 'ruby-production-breakpoints/platform'
|
9
|
+
require 'ruby-production-breakpoints/errors'
|
10
|
+
require 'ruby-production-breakpoints/breakpoints'
|
11
|
+
require 'ruby-production-breakpoints/configuration'
|
12
|
+
require 'ruby-production-breakpoints/parser'
|
13
|
+
require 'ruby-production-breakpoints/method_override'
|
14
|
+
require 'ruby-production-breakpoints/start_end_line_validator'
|
15
|
+
|
16
|
+
module ProductionBreakpoints
|
17
|
+
extend self
|
18
|
+
|
19
|
+
attr_accessor :installed_breakpoints
|
20
|
+
|
21
|
+
self.installed_breakpoints = {} # FIXME: namespace by provider, to allow multiple BP per file
|
22
|
+
|
23
|
+
def configure
|
24
|
+
@configuration = Configuration.new
|
25
|
+
yield @configuration
|
26
|
+
@configuration.finish!
|
27
|
+
end
|
28
|
+
|
29
|
+
def config
|
30
|
+
unless defined?(@configuration)
|
31
|
+
raise NotConfiguredError
|
32
|
+
end
|
33
|
+
|
34
|
+
@configuration
|
35
|
+
end
|
36
|
+
|
37
|
+
# For now add new types here
|
38
|
+
def install_breakpoint(type, source_file, start_line, end_line, trace_id: 1)
|
39
|
+
# Hack to check if there is a supported breakpoint of this type for now
|
40
|
+
case type.name
|
41
|
+
when 'ProductionBreakpoints::Breakpoints::Latency'
|
42
|
+
when 'ProductionBreakpoints::Breakpoints::Inspect'
|
43
|
+
when 'ProductionBreakpoints::Breakpoints::Locals'
|
44
|
+
# logger.debug("Creating latency tracer")
|
45
|
+
# now rewrite source to call this created breakpoint through parser
|
46
|
+
else
|
47
|
+
config.logger.error("Unsupported breakpoint type #{type}")
|
48
|
+
end
|
49
|
+
|
50
|
+
breakpoint = type.new(source_file, start_line, end_line, trace_id: trace_id)
|
51
|
+
installed_breakpoints[trace_id.to_sym] = breakpoint
|
52
|
+
breakpoint.install
|
53
|
+
breakpoint.load
|
54
|
+
end
|
55
|
+
|
56
|
+
def disable_breakpoint(trace_id)
|
57
|
+
breakpoint = installed_breakpoints.delete(trace_id)
|
58
|
+
breakpoint.unload
|
59
|
+
breakpoint.uninstall
|
60
|
+
end
|
61
|
+
|
62
|
+
def disable!
|
63
|
+
installed_breakpoints.each do |trace_id, _bp|
|
64
|
+
disable_breakpoint(trace_id)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def sync!
|
69
|
+
# FIXME: don't just install, also remove - want to 'resync'
|
70
|
+
# logger.debug("Resync initiated")
|
71
|
+
desired = config.configured_breakpoints['breakpoints']
|
72
|
+
|
73
|
+
desired_trace_ids = desired.map { |bp| bp['trace_id'] }
|
74
|
+
installed_trace_ids = installed_breakpoints.keys
|
75
|
+
|
76
|
+
to_install_tids = desired_trace_ids - installed_trace_ids
|
77
|
+
to_remove_tids = installed_trace_ids - desired_trace_ids
|
78
|
+
to_install = desired.select { |bp| to_install_tids.include?(bp['trace_id']) }
|
79
|
+
# logger.debug("Will install #{to_install.size} breakpoints")
|
80
|
+
# logger.debug("Will remove #{to_remove_tids.size} breakpoints")
|
81
|
+
to_install.each do |bp|
|
82
|
+
handler = breakpoint_constant_for_type(bp)
|
83
|
+
unless valid_start_line_end_line?(bp['source_file'], bp['start_line'], bp['end_line'])
|
84
|
+
msg = <<~MSG
|
85
|
+
Skipping #{handler} for #{bp['source_file']}. start line and end line do not point to any code on the file.
|
86
|
+
MSG
|
87
|
+
config.logger.warn(msg)
|
88
|
+
next
|
89
|
+
end
|
90
|
+
install_breakpoint(handler, bp['source_file'], bp['start_line'], bp['end_line'], trace_id: bp['trace_id'])
|
91
|
+
end
|
92
|
+
|
93
|
+
to_remove_tids.each do |trace_id|
|
94
|
+
disable_breakpoint(trace_id)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
def valid_start_line_end_line?(source_file, start_line, end_line)
|
101
|
+
StartEndLineValidator.call(source_file, start_line, end_line)
|
102
|
+
end
|
103
|
+
|
104
|
+
def breakpoint_constant_for_type(bp)
|
105
|
+
symstr = "ProductionBreakpoints::Breakpoints::#{bp['type'].capitalize}"
|
106
|
+
Object.const_get(symstr)
|
107
|
+
rescue NameError
|
108
|
+
config.logger.error("Could not find breakpoint handler for #{symstr}")
|
109
|
+
end
|
110
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby-production-breakpoints
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dale Hamel
|
@@ -143,7 +143,21 @@ email: dale.hamel@srvthe.net
|
|
143
143
|
executables: []
|
144
144
|
extensions: []
|
145
145
|
extra_rdoc_files: []
|
146
|
-
files:
|
146
|
+
files:
|
147
|
+
- lib/ruby-production-breakpoints.rb
|
148
|
+
- lib/ruby-production-breakpoints/breakpoints.rb
|
149
|
+
- lib/ruby-production-breakpoints/breakpoints/base.rb
|
150
|
+
- lib/ruby-production-breakpoints/breakpoints/inspect.rb
|
151
|
+
- lib/ruby-production-breakpoints/breakpoints/latency.rb
|
152
|
+
- lib/ruby-production-breakpoints/breakpoints/locals.rb
|
153
|
+
- lib/ruby-production-breakpoints/breakpoints/ustack.rb
|
154
|
+
- lib/ruby-production-breakpoints/configuration.rb
|
155
|
+
- lib/ruby-production-breakpoints/errors.rb
|
156
|
+
- lib/ruby-production-breakpoints/method_override.rb
|
157
|
+
- lib/ruby-production-breakpoints/parser.rb
|
158
|
+
- lib/ruby-production-breakpoints/platform.rb
|
159
|
+
- lib/ruby-production-breakpoints/start_end_line_validator.rb
|
160
|
+
- lib/ruby-production-breakpoints/version.rb
|
147
161
|
homepage: https://github.com/dalehamel/ruby-production-breakpoints
|
148
162
|
licenses:
|
149
163
|
- MIT
|