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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 19fccf01c424b4f35e62b84e6d7aaf3978e0253d
4
- data.tar.gz: 5a12abd7a1eb3148e9fd067675ffacb4012f4dd4
3
+ metadata.gz: d05e6df669dd8a7fd238743c6480d6d9e54138a7
4
+ data.tar.gz: a77994e9bee36a609e79359749047cbccebcf7be
5
5
  SHA512:
6
- metadata.gz: 481c6763053abdaea1057eba44c38003bc8475010a9684a02c82e5f5e55b217b960f93f29250c19a6cd5306aca4aac8bc6f644c1cd709e86017355a09504f3ac
7
- data.tar.gz: 61f3f7a14e828a8192a21836f8f9325ebba844a091390b46e3540673476d8492db06f68f1bf70fa420ee9755beb22308276cdb1aa34abe5356a5dad8190c2f96
6
+ metadata.gz: a183844d91ee89fbbb524eb5a52c64978787062d00b7e25f4c218f6f2e2401e2fd6ae65523e683d54896db06b425d6f2bbf7519f32d36e44103d624c8c5b57b0
7
+ data.tar.gz: d50b4ac8ea8079587d4a5be1a7d006f8e2cbe7fb169156103b03878baf10f4501a0859c8ac1f7dc6141d2c40282ef2853823cc3e1c07a51924cd1d05e4ccdd5a
data/README.md CHANGED
@@ -1 +1,62 @@
1
- # Ladybug
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
+
@@ -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
- def initialize
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 ID
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
- filename: filename,
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[:id]
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
- def trace_func
75
- @step_over_file = nil
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
- break_on_step_over = (@step_over_file == filename)
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
- @step_over_file = nil
217
+ @break = nil
133
218
  break
134
219
  when 'step_over'
135
- # todo: does "step over" really mean break in the same file,
136
- # or is there are a deeper meaning
137
- @step_over_file = filename
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
@@ -16,7 +16,8 @@ module Ladybug
16
16
  @scripts = {}
17
17
 
18
18
  @script_repository = ScriptRepository.new
19
- @debugger = Debugger.new
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
- # Just echo back the location the browser requested
83
- result = { locations: [ data["params"]["start"] ] }
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
- puts "setting breakpoint on #{script.path}:#{ruby_line_number}"
95
-
96
- breakpoint_id = @debugger.set_breakpoint(
97
- filename: script.absolute_path,
98
- line_number: ruby_line_number
99
- )
114
+ begin
115
+ breakpoint = @debugger.set_breakpoint(
116
+ filename: script.path,
117
+ line_number: ruby_line_number
118
+ )
100
119
 
101
- result = {
102
- breakpointId: breakpoint_id,
103
- locations: [
104
- {
105
- scriptId: script.id,
106
- lineNumber: line_number,
107
- columnNumber: data["params"]["columnNumber"],
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.0.1.alpha
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-21 00:00:00.000000000 Z
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: '0'
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: '0'
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: 1.3.1
84
+ version: '0'
57
85
  requirements: []
58
86
  rubyforge_project:
59
- rubygems_version: 2.6.10
87
+ rubygems_version: 2.6.7
60
88
  signing_key:
61
89
  specification_version: 4
62
90
  summary: Ladybug