ladybug 0.0.1.alpha → 0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
![screenshot](screenshots/demo.png)
|
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
|