ruby-production-breakpoints 0.0.5 → 0.0.6
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/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
|