ladybug 0.0.1.alpha → 0.1
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/README.md +62 -1
- data/lib/ladybug/debugger.rb +159 -14
- data/lib/ladybug/middleware.rb +53 -19
- metadata +37 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d05e6df669dd8a7fd238743c6480d6d9e54138a7
|
4
|
+
data.tar.gz: a77994e9bee36a609e79359749047cbccebcf7be
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a183844d91ee89fbbb524eb5a52c64978787062d00b7e25f4c218f6f2e2401e2fd6ae65523e683d54896db06b425d6f2bbf7519f32d36e44103d624c8c5b57b0
|
7
|
+
data.tar.gz: d50b4ac8ea8079587d4a5be1a7d006f8e2cbe7fb169156103b03878baf10f4501a0859c8ac1f7dc6141d2c40282ef2853823cc3e1c07a51924cd1d05e4ccdd5a
|
data/README.md
CHANGED
@@ -1 +1,62 @@
|
|
1
|
-
#
|
1
|
+
# 🐞 ladybug
|
2
|
+
|
3
|
+
ladybug is a visual debugger for Ruby web applications that uses
|
4
|
+
Chrome Devtools as a user interface.
|
5
|
+
|
6
|
+
It aims to provide a rich backend debugging experience in a UI that many
|
7
|
+
web developers are already familiar with for debugging frontend Javascript.
|
8
|
+
|
9
|
+
**This project is currently in a very early experimental phase.** Expect many limitations and bugs, and use at your own risk. If you try it out, please file
|
10
|
+
Github issues or [email me](mailto:gklitt@gmail.com) to help make this a
|
11
|
+
more useful tool.
|
12
|
+
|
13
|
+

|
14
|
+
|
15
|
+
## Get started
|
16
|
+
|
17
|
+
1) Install the gem:
|
18
|
+
|
19
|
+
`gem install --pre ladybug`
|
20
|
+
|
21
|
+
2) ladybug is implemented as a Rack middleware, so you'll need to add
|
22
|
+
`Ladybug::Middleware` to the Rack middleware stack.
|
23
|
+
For example, in Rails 5, add
|
24
|
+
the following line to `config/application.rb`:
|
25
|
+
|
26
|
+
```
|
27
|
+
config.middleware.insert_before(Rack::Sendfile, Ladybug::Middleware)
|
28
|
+
```
|
29
|
+
|
30
|
+
3) Make sure you're using the puma web server, which is currently the
|
31
|
+
only server that ladybug has been tested with.
|
32
|
+
|
33
|
+
To use puma in Rails: add `gem 'puma'` to your `Gemfile` and `bundle install`.
|
34
|
+
|
35
|
+
4) Then start up your server and try making a request.
|
36
|
+
You should see your server program output something like:
|
37
|
+
|
38
|
+
`Debug in Chrome: chrome-devtools://devtools/bundled/inspector.html?ws=localhost:3000`
|
39
|
+
|
40
|
+
5) Navigate to that URL, and you'll see a Chrome Devtools window.
|
41
|
+
In the Sources tab, you can view your Ruby source code.
|
42
|
+
If you set a breakpoint and then make another request to your server,
|
43
|
+
it should pause on the breakpoint and you'll be able to inspect
|
44
|
+
some variables in Devtools.
|
45
|
+
|
46
|
+
**Security warning:** This debugger should only be run in local development.
|
47
|
+
Running it on a server open to the internet could allow anyone to
|
48
|
+
execute code on your server without authenticating.
|
49
|
+
|
50
|
+
## Development status
|
51
|
+
|
52
|
+
* basic pause/continue breakpoint control is supported, but "step over" and "step into" aren't fully supported yet.
|
53
|
+
* inspecting primtive objects like strings and numbers works okay; support for more complex objects is in development.
|
54
|
+
* So far, ladybug has only been tested with simple Rails applications running on
|
55
|
+
Rails 5 with the puma web server. Eventually it aims to support more Rack
|
56
|
+
applications and web servers (and perhaps even non-Rack applications).
|
57
|
+
|
58
|
+
## Author
|
59
|
+
|
60
|
+
[@geoffreylitt](https://twitter.com/geoffreylitt)
|
61
|
+
|
62
|
+
|
data/lib/ladybug/debugger.rb
CHANGED
@@ -1,9 +1,20 @@
|
|
1
1
|
# A simple debugger using set_trace_func
|
2
2
|
# Allows for external control while in a breakpoint
|
3
3
|
|
4
|
+
require 'parser/current'
|
5
|
+
Parser::Builders::Default.emit_lambda = true
|
6
|
+
Parser::Builders::Default.emit_procarg0 = true
|
7
|
+
|
8
|
+
require 'memoist'
|
9
|
+
|
4
10
|
module Ladybug
|
5
11
|
class Debugger
|
6
|
-
|
12
|
+
extend Memoist
|
13
|
+
|
14
|
+
# preload_paths (optional):
|
15
|
+
# paths to pre-parse into Ruby code so that
|
16
|
+
# later we can quickly respond to requests for breakpoint locations
|
17
|
+
def initialize(preload_paths: [])
|
7
18
|
@breakpoints = []
|
8
19
|
|
9
20
|
@to_main_thread = Queue.new
|
@@ -11,6 +22,15 @@ module Ladybug
|
|
11
22
|
|
12
23
|
@on_pause = -> {}
|
13
24
|
@on_resume = -> {}
|
25
|
+
|
26
|
+
@parsed_files = {}
|
27
|
+
|
28
|
+
# Todo: consider thread safety of mutating this hash
|
29
|
+
Thread.new do
|
30
|
+
preload_paths.each do |preload_path|
|
31
|
+
parse(preload_path)
|
32
|
+
end
|
33
|
+
end
|
14
34
|
end
|
15
35
|
|
16
36
|
def start
|
@@ -37,6 +57,14 @@ module Ladybug
|
|
37
57
|
@to_main_thread.push({ command: 'step_over' })
|
38
58
|
end
|
39
59
|
|
60
|
+
def step_into
|
61
|
+
@to_main_thread.push({ command: 'step_into' })
|
62
|
+
end
|
63
|
+
|
64
|
+
def step_out
|
65
|
+
@to_main_thread.push({ command: 'step_out' })
|
66
|
+
end
|
67
|
+
|
40
68
|
def evaluate(expression)
|
41
69
|
@to_main_thread.push({
|
42
70
|
command: 'eval',
|
@@ -49,17 +77,34 @@ module Ladybug
|
|
49
77
|
@from_main_thread.pop
|
50
78
|
end
|
51
79
|
|
52
|
-
# returns a breakpoint
|
80
|
+
# Sets a breakpoint and returns a breakpoint object.
|
53
81
|
def set_breakpoint(filename:, line_number:)
|
82
|
+
possible_lines = line_numbers_with_code(filename)
|
83
|
+
|
84
|
+
# It's acceptable for a caller to request setting a breakpoint
|
85
|
+
# on a location where it's not possible to set a breakpoint.
|
86
|
+
# In this case, we set a breakpoint on the next possible location.
|
87
|
+
if !possible_lines.include?(line_number)
|
88
|
+
line_number = possible_lines.sort.find { |ln| ln > line_number }
|
89
|
+
end
|
90
|
+
|
91
|
+
# Sometimes the above check will fail and there's no possible breakpoint.
|
92
|
+
if line_number.nil?
|
93
|
+
raise InvalidBreakpointLocationError,
|
94
|
+
"invalid breakpoint line: #{filename}:#{line_number}"
|
95
|
+
end
|
96
|
+
|
54
97
|
breakpoint = {
|
55
|
-
|
98
|
+
# We need to use the absolute path here
|
99
|
+
# because set_trace_func ends up checking against that
|
100
|
+
filename: File.absolute_path(filename),
|
56
101
|
line_number: line_number,
|
57
|
-
id: "filename:#{line_number}"
|
102
|
+
id: "#{filename}:#{line_number}"
|
58
103
|
}
|
59
104
|
|
60
105
|
@breakpoints.push(breakpoint)
|
61
106
|
|
62
|
-
breakpoint
|
107
|
+
breakpoint
|
63
108
|
end
|
64
109
|
|
65
110
|
def remove_breakpoint(breakpoint_id)
|
@@ -69,11 +114,53 @@ module Ladybug
|
|
69
114
|
@breakpoints.delete_if { |bp| bp[:id] == breakpoint_id }
|
70
115
|
end
|
71
116
|
|
117
|
+
# Given a filename line number range of a requested breakpoint,
|
118
|
+
# give the line numbers of possible breakpoints.
|
119
|
+
#
|
120
|
+
# In practice, start and end number tend to be the same when
|
121
|
+
# Chrome devtools is the client.
|
122
|
+
#
|
123
|
+
# A breakpoint can be set at the beginning of any Ruby statement.
|
124
|
+
# (more details in #line_numbers_with_code)
|
125
|
+
def get_possible_breakpoints(path:, start_num:, end_num:)
|
126
|
+
(start_num..end_num).to_a & line_numbers_with_code(path)
|
127
|
+
end
|
128
|
+
|
72
129
|
private
|
73
130
|
|
74
|
-
|
75
|
-
|
131
|
+
# remove ladybug code from a callstack and prepare it for comparison
|
132
|
+
# this is a hack implemenetation for now, can be made better
|
133
|
+
def clean(callstack)
|
134
|
+
callstack.drop_while { |frame| frame.to_s.include? "ladybug" }
|
135
|
+
end
|
136
|
+
|
137
|
+
# If we're in step over/in/out mode,
|
138
|
+
# detect if we should break even if there's not a breakpoint set here
|
139
|
+
def break_on_step?
|
140
|
+
# This is an important early return;
|
141
|
+
# we don't want to do anything w/ the callstack unless
|
142
|
+
# we're looking for a breakpoint, because
|
143
|
+
# that adds time to every single line of code execution
|
144
|
+
# which makes things really slow
|
145
|
+
return false if @break.nil?
|
146
|
+
|
147
|
+
bp_callstack = clean(@breakpoint_callstack)
|
148
|
+
current_callstack = clean(Thread.current.backtrace_locations)
|
149
|
+
|
150
|
+
if @break == 'step_over'
|
151
|
+
return bp_callstack[1].to_s == current_callstack[1].to_s
|
152
|
+
elsif @break == 'step_into'
|
153
|
+
return stacks_equal?(bp_callstack, current_callstack[1..-1])
|
154
|
+
elsif @break == 'step_out'
|
155
|
+
return stacks_equal?(current_callstack, bp_callstack[1..-1])
|
156
|
+
end
|
157
|
+
end
|
76
158
|
|
159
|
+
def stacks_equal?(stack1, stack2)
|
160
|
+
stack1.map(&:to_s) == stack2.map(&:to_s)
|
161
|
+
end
|
162
|
+
|
163
|
+
def trace_func
|
77
164
|
proc { |event, filename, line_number, id, binding, klass, *rest|
|
78
165
|
# This check is called a lot so perhaps worth making faster,
|
79
166
|
# but might not matter much with small number of breakpoints in practice
|
@@ -81,9 +168,7 @@ module Ladybug
|
|
81
168
|
bp[:filename] == filename && bp[:line_number] == line_number
|
82
169
|
end
|
83
170
|
|
84
|
-
|
85
|
-
|
86
|
-
if breakpoint_hit || break_on_step_over
|
171
|
+
if breakpoint_hit || break_on_step?
|
87
172
|
local_variables =
|
88
173
|
binding.local_variables.each_with_object({}) do |lvar, hash|
|
89
174
|
hash[lvar] = binding.local_variable_get(lvar)
|
@@ -129,12 +214,19 @@ module Ladybug
|
|
129
214
|
|
130
215
|
case message[:command]
|
131
216
|
when 'continue'
|
132
|
-
@
|
217
|
+
@break = nil
|
133
218
|
break
|
134
219
|
when 'step_over'
|
135
|
-
|
136
|
-
|
137
|
-
|
220
|
+
@break = 'step_over'
|
221
|
+
@breakpoint_callstack = Thread.current.backtrace_locations
|
222
|
+
break
|
223
|
+
when 'step_into'
|
224
|
+
@break = 'step_into'
|
225
|
+
@breakpoint_callstack = Thread.current.backtrace_locations
|
226
|
+
break
|
227
|
+
when 'step_out'
|
228
|
+
@break = 'step_out'
|
229
|
+
@breakpoint_callstack = Thread.current.backtrace_locations
|
138
230
|
break
|
139
231
|
when 'eval'
|
140
232
|
evaluated =
|
@@ -152,5 +244,58 @@ module Ladybug
|
|
152
244
|
end
|
153
245
|
}
|
154
246
|
end
|
247
|
+
|
248
|
+
# get valid breakpoint lines for a file, with a memoize cache
|
249
|
+
# todo: we don't really need to cache this; parsing is the slow part
|
250
|
+
def line_numbers_with_code(path)
|
251
|
+
ast = parse(path)
|
252
|
+
single_statement_lines(ast)
|
253
|
+
end
|
254
|
+
|
255
|
+
def parse(path)
|
256
|
+
if !@parsed_files.key?(path)
|
257
|
+
code = File.read(path)
|
258
|
+
@parsed_files[path] = Parser::CurrentRuby.parse(code)
|
259
|
+
end
|
260
|
+
|
261
|
+
@parsed_files[path]
|
262
|
+
end
|
263
|
+
|
264
|
+
# A breakpoint can be set at the beginning of any node where there is no
|
265
|
+
# begin (i.e. multi-line) node anywhere under the node
|
266
|
+
def single_statement_lines(ast)
|
267
|
+
child_types = deep_child_node_types(ast)
|
268
|
+
|
269
|
+
if !child_types.include?(:begin) && !child_types.include?(:kwbegin)
|
270
|
+
expr = ast.loc.expression
|
271
|
+
|
272
|
+
if !expr.nil?
|
273
|
+
expr.begin.line
|
274
|
+
else
|
275
|
+
nil
|
276
|
+
end
|
277
|
+
else
|
278
|
+
ast.children.
|
279
|
+
select { |child| child.is_a? AST::Node }.
|
280
|
+
flat_map { |child| single_statement_lines(child) }.
|
281
|
+
compact.
|
282
|
+
uniq
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
# Return all unique types of AST nodes under this node,
|
287
|
+
# including the node itself.
|
288
|
+
#
|
289
|
+
# We memoize this because we repeatedly hit this for each AST
|
290
|
+
def deep_child_node_types(ast)
|
291
|
+
types = ast.children.flat_map do |child|
|
292
|
+
deep_child_node_types(child) if child.is_a? AST::Node
|
293
|
+
end.compact + [ast.type]
|
294
|
+
|
295
|
+
types.uniq
|
296
|
+
end
|
297
|
+
memoize :deep_child_node_types
|
298
|
+
|
299
|
+
class InvalidBreakpointLocationError < StandardError; end
|
155
300
|
end
|
156
301
|
end
|
data/lib/ladybug/middleware.rb
CHANGED
@@ -16,7 +16,8 @@ module Ladybug
|
|
16
16
|
@scripts = {}
|
17
17
|
|
18
18
|
@script_repository = ScriptRepository.new
|
19
|
-
@debugger =
|
19
|
+
@debugger =
|
20
|
+
Debugger.new(preload_paths: @script_repository.all.map(&:path))
|
20
21
|
@object_manager = ObjectManager.new
|
21
22
|
end
|
22
23
|
|
@@ -79,8 +80,27 @@ module Ladybug
|
|
79
80
|
scriptSource: File.new(path, "r").read
|
80
81
|
}
|
81
82
|
elsif data["method"] == "Debugger.getPossibleBreakpoints"
|
82
|
-
|
83
|
-
|
83
|
+
script = @script_repository.find(id: data["params"]["start"]["scriptId"])
|
84
|
+
|
85
|
+
# we convert to/from 0-indexed line numbers in Chrome
|
86
|
+
# at the earliest/latest possible moment;
|
87
|
+
# in this gem, lines are 1-indexed
|
88
|
+
start_num = data["params"]["start"]["lineNumber"] + 1
|
89
|
+
end_num = data["params"]["end"]["lineNumber"] + 1
|
90
|
+
|
91
|
+
breakpoint_lines = @debugger.get_possible_breakpoints(
|
92
|
+
path: script.path, start_num: start_num, end_num: end_num
|
93
|
+
)
|
94
|
+
|
95
|
+
locations = breakpoint_lines.map do |breakpoint_line|
|
96
|
+
{
|
97
|
+
scriptId: script.id,
|
98
|
+
lineNumber: breakpoint_line - 1,
|
99
|
+
columnNumber: 0
|
100
|
+
}
|
101
|
+
end
|
102
|
+
|
103
|
+
result = { locations: locations }
|
84
104
|
elsif data["method"] == "Debugger.setBreakpointByUrl"
|
85
105
|
# Chrome gives us a virtual URL;
|
86
106
|
# we need an absolute path to the file to match the API for set_trace_func
|
@@ -91,23 +111,27 @@ module Ladybug
|
|
91
111
|
line_number = data["params"]["lineNumber"]
|
92
112
|
ruby_line_number = line_number + 1
|
93
113
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
)
|
114
|
+
begin
|
115
|
+
breakpoint = @debugger.set_breakpoint(
|
116
|
+
filename: script.path,
|
117
|
+
line_number: ruby_line_number
|
118
|
+
)
|
100
119
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
120
|
+
result = {
|
121
|
+
breakpointId: breakpoint[:id],
|
122
|
+
locations: [
|
123
|
+
{
|
124
|
+
scriptId: script.id,
|
125
|
+
# todo: need to get these +/- transformations centralized.
|
126
|
+
# a LineNumber class might be necessary...
|
127
|
+
lineNumber: breakpoint[:line_number] - 1,
|
128
|
+
columnNumber: data["params"]["columnNumber"],
|
129
|
+
}
|
130
|
+
]
|
131
|
+
}
|
132
|
+
rescue Debugger::InvalidBreakpointLocationError
|
133
|
+
result = {}
|
134
|
+
end
|
111
135
|
elsif data["method"] == "Debugger.resume"
|
112
136
|
# Synchronously just ack the command;
|
113
137
|
# we'll async hear back from the main thread when execution resumes
|
@@ -118,6 +142,16 @@ module Ladybug
|
|
118
142
|
# we'll async hear back from the main thread when execution resumes
|
119
143
|
@debugger.step_over
|
120
144
|
result = {}
|
145
|
+
elsif data["method"] == "Debugger.stepInto"
|
146
|
+
# Synchronously just ack the command;
|
147
|
+
# we'll async hear back from the main thread when execution resumes
|
148
|
+
@debugger.step_into
|
149
|
+
result = {}
|
150
|
+
elsif data["method"] == "Debugger.stepOut"
|
151
|
+
# Synchronously just ack the command;
|
152
|
+
# we'll async hear back from the main thread when execution resumes
|
153
|
+
@debugger.step_out
|
154
|
+
result = {}
|
121
155
|
elsif data["method"] == "Debugger.evaluateOnCallFrame"
|
122
156
|
evaluated = @debugger.evaluate(data["params"]["expression"])
|
123
157
|
result = {
|
metadata
CHANGED
@@ -1,29 +1,57 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ladybug
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: '0.1'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Geoffrey Litt
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-01-
|
11
|
+
date: 2018-01-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: faye-websocket
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: 0.10.7
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - "
|
24
|
+
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
26
|
+
version: 0.10.7
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: parser
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 2.4.0.2
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 2.4.0.2
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: memoist
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.14.0
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.14.0
|
27
55
|
description: Debug Ruby code using Chrome Devtools
|
28
56
|
email: gklitt@gmail.com
|
29
57
|
executables: []
|
@@ -51,12 +79,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
51
79
|
version: '0'
|
52
80
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
53
81
|
requirements:
|
54
|
-
- - "
|
82
|
+
- - ">="
|
55
83
|
- !ruby/object:Gem::Version
|
56
|
-
version:
|
84
|
+
version: '0'
|
57
85
|
requirements: []
|
58
86
|
rubyforge_project:
|
59
|
-
rubygems_version: 2.6.
|
87
|
+
rubygems_version: 2.6.7
|
60
88
|
signing_key:
|
61
89
|
specification_version: 4
|
62
90
|
summary: Ladybug
|