byebug-dap 0.1.2 → 0.1.3

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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -3
  3. data/README.md +5 -5
  4. data/bin/byebug-dap +25 -8
  5. data/lib/byebug/dap.rb +48 -18
  6. data/lib/byebug/dap/command.rb +250 -0
  7. data/lib/byebug/dap/command_processor.rb +50 -49
  8. data/lib/byebug/dap/commands/attach.rb +13 -0
  9. data/lib/byebug/dap/commands/breakpoint_locations.rb +28 -0
  10. data/lib/byebug/dap/commands/configuration_done.rb +12 -0
  11. data/lib/byebug/dap/commands/continue.rb +18 -0
  12. data/lib/byebug/dap/commands/disconnect.rb +16 -0
  13. data/lib/byebug/dap/commands/evaluate.rb +27 -0
  14. data/lib/byebug/dap/commands/exception_info.rb +40 -0
  15. data/lib/byebug/dap/commands/initialize.rb +27 -0
  16. data/lib/byebug/dap/commands/launch.rb +13 -0
  17. data/lib/byebug/dap/commands/next.rb +20 -0
  18. data/lib/byebug/dap/commands/pause.rb +20 -0
  19. data/lib/byebug/dap/commands/scopes.rb +56 -0
  20. data/lib/byebug/dap/commands/set_breakpoints.rb +48 -0
  21. data/lib/byebug/dap/commands/set_exception_breakpoints.rb +28 -0
  22. data/lib/byebug/dap/commands/set_function_breakpoints.rb +96 -0
  23. data/lib/byebug/dap/commands/source.rb +12 -0
  24. data/lib/byebug/dap/commands/stack_trace.rb +50 -0
  25. data/lib/byebug/dap/commands/step_in.rb +24 -0
  26. data/lib/byebug/dap/commands/step_out.rb +21 -0
  27. data/lib/byebug/dap/commands/threads.rb +20 -0
  28. data/lib/byebug/dap/commands/variables.rb +33 -0
  29. data/lib/byebug/dap/contextual_command.rb +30 -0
  30. data/lib/byebug/dap/helpers/captured_io.rb +65 -0
  31. data/lib/byebug/dap/helpers/captured_output.rb +21 -0
  32. data/lib/byebug/dap/{channel.rb → helpers/channel.rb} +0 -0
  33. data/lib/byebug/dap/{child_spawned_event_body.rb → helpers/child_spawned_event_body.rb} +0 -0
  34. data/lib/byebug/dap/{handles.rb → helpers/handles.rb} +0 -0
  35. data/lib/byebug/dap/{invalid_request_argument_error.rb → helpers/invalid_request_argument_error.rb} +0 -0
  36. data/lib/byebug/dap/helpers/safe_helpers.rb +17 -0
  37. data/lib/byebug/dap/helpers/scalar.rb +19 -0
  38. data/lib/byebug/dap/helpers/stdio.rb +21 -0
  39. data/lib/byebug/dap/helpers/value_helpers.rb +60 -0
  40. data/lib/byebug/dap/server.rb +52 -36
  41. data/lib/byebug/dap/session.rb +176 -0
  42. data/lib/byebug/gem.rb +11 -0
  43. metadata +38 -10
  44. data/lib/byebug/dap/controller.rb +0 -252
  45. data/lib/byebug/dap/interface.rb +0 -303
  46. data/lib/byebug/dap/safe_helpers.rb +0 -55
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cdcb974e222d9a64fea6f7f070e0352dc746dfd6f331d4b7230aad278ba563db
4
- data.tar.gz: 91af303a92685b001c2fee3039f47cba119b67f3edefdeb11b9f28d54d6e2608
3
+ metadata.gz: 18d52dd5f136afc3350836a23abe3c9b957646358a566e15bfac0aedd070189c
4
+ data.tar.gz: 615c83e3512cc884f1d9dd2661b578cdf4314ebe2f3bab55f4c76f70a7fb17fc
5
5
  SHA512:
6
- metadata.gz: c29328a148dc9e690452fdb09acbb5f63a6612a85f1e5c5cfa7a14b72ed5517b99d0fca4077ca3001d9d4cfbe0b25afa9f3475d65d21ee01369cb623af8462a4
7
- data.tar.gz: 79f5f2dcfb469ab62250543620473f34c9dce69204eef8bb27cfeb1b636041716effb1f1486cd50ebb1c72e2770b627be5361112e2e64a6b97fd4d93fa9768d4
6
+ metadata.gz: f5932e26ae7fe7f9518e35df4a22aad7cb363f57d56680e8416e2f6dfa3aed447b7a38270e752313b08a7244108a13f64daee74b58d715ce333b95e4230abbf8
7
+ data.tar.gz: 4e150cd4ba325258f41d91496ab6d6914294d15330a124c1501ec50b28991a8619d49be3420d4f22305c5067c5d42de167a62dbf2a919b501af9dcb75c18f6f1
@@ -1,13 +1,24 @@
1
1
  # Change Log
2
2
 
3
+ ## 0.1.3
4
+
5
+ - Support for output capture
6
+ - Support for setting function breakpoints
7
+ - Support for breakpoint locations request
8
+ - Support for delayed stack trace loading
9
+ - Basic support for exception breakpoints
10
+ - Support for conditional breakpoints
11
+ - Support for hit conditional breakpoints
12
+ - Support for logpoints
13
+
3
14
  ## 0.1.2
4
15
 
5
16
  - Fix possible failure when a breakpoint is hit but can't be resolved
6
17
  - Fix possible failure when frame arguments can't be evaluated
7
18
  - Exit on disconnect when started by 'launch'
8
- - Expose `Server#wait_for_client` instead of passing a block
9
- - Expose `Interface#stop!` to allow the debugee to stop
10
- - Support specifying a start sequence with `--on-start CODE`
19
+ - Expose `Byebug::DAP::Server#wait_for_client` instead of passing a block
20
+ - Expose `Byebug::DAP#stop!` to allow the debugee to stop
21
+ - Support for specifying a start sequence with `--on-start CODE`
11
22
  - Support for child processes
12
23
 
13
24
  ## 0.1.1
data/README.md CHANGED
@@ -5,8 +5,8 @@ Protocol](https://microsoft.github.io/debug-adapter-protocol) support to Byebug.
5
5
 
6
6
  ## TODO
7
7
 
8
- - Multi-process support
9
- - Stdout/stderr
10
- - In STDIO mode, spawn with extra FDs and use those instead of 0/1?
11
- - Many DAP features are already supported by Byebug and just need to be
12
- implemented in DAP mode
8
+ - In STDIO mode, spawn with extra FDs and use those instead of 0/1?
9
+ - Set class-only or instance-only method breakpoints. Blocked by
10
+ [byebug#734](https://github.com/deivid-rodriguez/byebug/issues/734).
11
+ - Support advanced exception breakpoints. Requires client support (VSCode
12
+ extension).
@@ -11,14 +11,20 @@ def next_arg
11
11
  arg = ARGV.pop
12
12
  return arg if arg
13
13
 
14
- STDERR.puts USAGE
14
+ LOG.puts USAGE
15
15
  exit!
16
16
  end
17
17
 
18
18
  options = {}
19
+
19
20
  OptionParser.new do |opts|
20
21
  opts.banner = USAGE
21
22
 
23
+ opts.on("--version", "Print version information") do |v|
24
+ puts "#{Byebug::DAP::NAME} #{Byebug::DAP::VERSION}"
25
+ exit
26
+ end
27
+
22
28
  opts.on("--stdio", "Listen on STDIN and STDOUT") { |v| options[:stdio] = v }
23
29
  opts.on("--listen PORT", "Listen on a TCP port") { |v| options[:listen] = v }
24
30
  opts.on("--unix SOCKET", "Listen on a unix socket") { |v| options[:unix] = v }
@@ -27,8 +33,14 @@ OptionParser.new do |opts|
27
33
  opts.on("--debug-protocol", "Debug DAP") { |v| Byebug::DAP::Debug.protocol = true if v }
28
34
  opts.on("--debug-evaluate", "Debug variable evaluation") { |v| Byebug::DAP::Debug.evaluate = true if v }
29
35
  opts.on("--on-start CODE", "Code to print once the debugger is available") { |v| options[:start_code] = v }
36
+ opts.on("--capture-output", "Capture stdout and stderr") { |v| options[:capture_output] = v }
37
+ opts.on("--supress-output", "Supress stdout and stderr when capturing") { |v| options[:suppress_output] = v }
38
+
39
+ opts.on("--log FD", "Log to the specified file descriptor") { |v| Kernel::LOG = IO.new(v.to_i) }
30
40
  end.parse!
31
41
 
42
+ Kernel::LOG = STDERR.dup unless defined?(Kernel::LOG)
43
+
32
44
  program = next_arg
33
45
  if program == '-'
34
46
  program = next_arg
@@ -55,27 +67,32 @@ elsif options[:unix]
55
67
  end
56
68
 
57
69
  else
58
- STDERR.puts USAGE, "One of --stdio, --listen, or --unix is required"
70
+ LOG.puts USAGE, "One of --stdio, --listen, or --unix is required"
59
71
  exit!
60
72
  end
61
73
 
62
74
  begin
63
- STDERR.print "Starting DAP... " unless options[:start_code]
75
+ LOG.print "Starting DAP... " unless options[:start_code]
64
76
 
65
- server = Byebug.start_dap(host, port)
77
+ server = Byebug::DAP::Server.new(capture: options[:capture_output], forward: !options[:suppress_output])
78
+ server.start(host, port)
66
79
 
67
- STDERR.puts options[:start_code] if options[:start_code]
68
- STDERR.flush
80
+ LOG.puts options[:start_code] if options[:start_code]
69
81
 
70
82
  if options[:wait]
71
- STDERR.print "waiting for debugger... " unless options[:start_code]
83
+ LOG.print "waiting for debugger... " unless options[:start_code]
84
+ hINT = Signal.trap("INT") { LOG.puts; exit }
72
85
  server.wait_for_client
86
+ Signal.trap("INT", hINT)
73
87
  end
74
88
 
75
- STDERR.puts "ok" unless options[:start_code]
89
+ LOG.puts "ok" unless options[:start_code]
76
90
 
77
91
  require File.realpath(program)
78
92
 
93
+ rescue => e
94
+ LOG.puts "#{e.message} (#{e.class.name})", *e.backtrace
95
+
79
96
  ensure
80
97
  File.delete(port) if File.exist?(port) if host == :unix
81
98
  end
@@ -3,32 +3,62 @@ require 'byebug'
3
3
  require 'byebug/core'
4
4
  require 'byebug/remote'
5
5
 
6
- require_relative 'dap/channel'
7
- require_relative 'dap/child_spawned_event_body'
8
- require_relative 'dap/handles'
9
- require_relative 'dap/invalid_request_argument_error'
10
- require_relative 'dap/safe_helpers'
6
+ require_relative 'gem'
11
7
 
12
- require_relative 'dap/server'
8
+ # load helpers
9
+ Dir[File.join(__dir__, 'dap', 'helpers', '*.rb')].each { |file| require file }
10
+
11
+ # load command base classes
12
+ require_relative 'dap/command'
13
+ require_relative 'dap/contextual_command'
14
+
15
+ # load commands
16
+ Dir[File.join(__dir__, 'dap', 'commands', '*.rb')].each { |file| require file }
17
+
18
+ # load everything else
13
19
  require_relative 'dap/command_processor'
14
- require_relative 'dap/controller'
15
- require_relative 'dap/interface'
20
+ require_relative 'dap/session'
21
+ require_relative 'dap/server'
16
22
 
17
23
  module Byebug
18
- module DAP
19
- module Debug
20
- class << self
21
- @protocol = false
22
- @evaluate = false
23
-
24
- attr_accessor :protocol, :evaluate
25
- end
24
+ class << self
25
+ def start_dap(host, port = 0, &block)
26
+ DAP::Server.new(&block).start(host, port)
26
27
  end
27
28
  end
28
29
 
30
+ class Context
31
+ public :processor
32
+ end
33
+
34
+ class Frame
35
+ attr_reader :context
36
+ end
37
+ end
38
+
39
+ module Byebug::DAP
40
+ Protocol = ::DAP
41
+
29
42
  class << self
30
- def start_dap(host, port = 0, &block)
31
- DAP::Server.new(&block).start(host, port)
43
+ def child_spawned(*args)
44
+ Session.child_spawned(*args)
45
+ end
46
+
47
+ def stop!
48
+ interface = Byebug::Context.interface
49
+ return false unless interface.is_a?(Session)
50
+
51
+ interface.stop!
52
+ true
32
53
  end
33
54
  end
34
55
  end
56
+
57
+ module Byebug::DAP::Debug
58
+ class << self
59
+ @protocol = false
60
+ @evaluate = false
61
+
62
+ attr_accessor :protocol, :evaluate
63
+ end
64
+ end
@@ -0,0 +1,250 @@
1
+ module Byebug::DAP
2
+ class Command
3
+ EVAL_ERROR = "*Error in evaluation*"
4
+
5
+ include SafeHelpers
6
+
7
+ def self.command
8
+ return @command_name if defined?(@command_name)
9
+
10
+ raise "Not a command" if self == Byebug::DAP::Command
11
+ raise "Not a command" unless self < Byebug::DAP::Command
12
+ raise "Not a command" unless self.name.start_with?('Byebug::DAP::Command::')
13
+
14
+ last = self.name.split('::').last
15
+ @command_name = "#{last[0].downcase}#{last[1..]}"
16
+ end
17
+
18
+ def self.register!
19
+ (@@commands ||= {})[command] = self
20
+ end
21
+
22
+ def self.resolve!(session, request)
23
+ cls = @@commands[request.command]
24
+ return cls if cls
25
+
26
+ session.respond! request, success: false, message: 'Invalid command'
27
+ end
28
+
29
+ def self.execute(session, request, *args)
30
+ return unless command = resolve!(session, request)
31
+
32
+ command.new(session, request, *args).safe_execute
33
+ end
34
+
35
+ def initialize(session, request)
36
+ @session = session
37
+ @request = request
38
+ end
39
+
40
+ def log(*args)
41
+ @session.log(*args)
42
+ end
43
+
44
+ def safe_execute
45
+ execute
46
+
47
+ rescue InvalidRequestArgumentError => e
48
+ message =
49
+ case e.error
50
+ when String
51
+ e.error
52
+
53
+ when :missing_argument
54
+ "Argument is unspecified: #{e.scope}"
55
+
56
+ when :missing_entry
57
+ "Cannot locate #{e.scope} ##{e.value}"
58
+
59
+ when :invalid_entry
60
+ "Error resolving #{e.scope}: #{e.value}"
61
+
62
+ else
63
+ log "#{e.message} (#{e.class})", *e.backtrace
64
+ "An internal error occured"
65
+ end
66
+
67
+ respond! success: false, message: message
68
+
69
+ rescue IOError, Errno::EPIPE, Errno::ECONNRESET, Errno::ECONNABORTED
70
+ :disconnected
71
+
72
+ rescue CommandProcessor::TimeoutError => e
73
+ respond! success: false, message: "Debugger on thread ##{e.context.thnum} is not responding"
74
+
75
+ rescue StandardError => e
76
+ respond! success: false, message: "An internal error occured"
77
+ log "#{e.message} (#{e.class})", *e.backtrace
78
+ end
79
+
80
+ private
81
+
82
+ def event!(*args, **values)
83
+ @session.event! *args, **values
84
+ return
85
+ end
86
+
87
+ def respond!(*args, **values)
88
+ raise "Cannot respond without a request" unless @request
89
+
90
+ @session.respond! @request, *args, **values
91
+ return
92
+ end
93
+
94
+ def stopped!
95
+ return if !Byebug.started?
96
+
97
+ respond! success: false, message: "Cannot #{@request.command} - debugger is already running"
98
+ end
99
+
100
+ def started!
101
+ return if Byebug.started?
102
+
103
+ respond! success: false, message: "Cannot #{@request.command} - debugger is not running"
104
+ end
105
+
106
+ def args
107
+ @request.arguments
108
+ end
109
+
110
+ def exception_description(ex)
111
+ safe(-> { "#{ex.message} (#{ex.class.name})" }, :call) { EVAL_ERROR }
112
+ end
113
+
114
+ def execute_on_thread(thnum, block, &on_error)
115
+ return safe(block, :call, &on_error) if thnum == 0 || @context&.thnum == thnum
116
+
117
+ p = find_thread(thnum).processor
118
+ safe(-> { p.execute(&block) }, :call, &on_error)
119
+ end
120
+
121
+ def find_thread(thnum)
122
+ raise InvalidRequestArgumentError.new(:missing_argument, scope: 'thread ID') unless thnum
123
+
124
+ ctx = Byebug.contexts.find { |c| c.thnum == thnum }
125
+ raise InvalidRequestArgumentError.new(:missing_entry, value: thnum, scope: 'thread') unless ctx
126
+
127
+ ctx
128
+ end
129
+
130
+ def find_frame(ctx, frnum)
131
+ raise InvalidRequestArgumentError.new(:missing_entry, value: frnum, scope: 'frame') unless frnum < ctx.stack_size
132
+
133
+ ::Byebug::Frame.new(ctx, frnum)
134
+ end
135
+
136
+ def resolve_frame_id(id)
137
+ raise InvalidRequestArgumentError.new(:missing_argument, scope: 'frame ID') unless id
138
+
139
+ entry = @session.restore_frame(id)
140
+ raise InvalidRequestArgumentError.new(:missing_entry, value: id, scope: 'frame ID') unless entry
141
+
142
+ thnum, frnum = entry
143
+ ctx = find_thread(thnum)
144
+ frame = find_frame(ctx, frnum)
145
+ return frame, thnum, frnum
146
+ end
147
+
148
+ def resolve_variables_reference(ref)
149
+ raise InvalidRequestArgumentError.new(:missing_argument, scope: 'variables reference') unless ref
150
+
151
+ entry = @session.restore_variables(ref)
152
+ raise InvalidRequestArgumentError.new(:missing_entry, value: ref, scope: 'variables reference') unless entry
153
+
154
+ thnum, frnum, kind, *entry = entry
155
+
156
+ case kind
157
+ when :locals
158
+ frame = find_frame(find_thread(thnum), frnum)
159
+ named, indexed = entry[0], []
160
+ get = ->(key) {
161
+ return frame._self if key == :self
162
+ return frame.context.processor.last_exception if key == :$!
163
+ values ||= frame.locals
164
+ values[key]
165
+ }
166
+
167
+ when :globals
168
+ frame = find_frame(find_thread(thnum), frnum)
169
+ named, indexed = entry[0], []
170
+ get = ->(key) { frame._binding.eval(key.to_s) }
171
+
172
+ when :variable, :evaluate
173
+ value, named, indexed = entry
174
+ get = ->(key) { value.instance_eval { binding }.eval(key.to_s) }
175
+ index = ->(key) { value[key] }
176
+
177
+ else
178
+ raise InvalidRequestArgumentError.new(:invalid_entry, value: kind, scope: 'variable scope')
179
+ end
180
+
181
+ return thnum, frnum, named.map { |k| [k, get] }, indexed.map { |k| [k, index] }
182
+ end
183
+
184
+ def can_read_file!(path)
185
+ path = File.realpath(path)
186
+ return path if File.readable?(path)
187
+
188
+ if File.exist?(path)
189
+ respond! success: false, message: "Source file '#{path}' exists but cannot be read"
190
+ else
191
+ respond! success: false, message: "No source file available for '#{path}'"
192
+ end
193
+
194
+ return nil
195
+ end
196
+
197
+ def potential_breakpoint_lines(path)
198
+ ::Byebug::Breakpoint.potential_lines(path)
199
+ rescue ScriptError, StandardError => e
200
+ yield(e)
201
+ end
202
+
203
+ def convert_breakpoint_condition(condition)
204
+ return nil if condition.nil? || condition.empty?
205
+ return nil unless condition.is_a?(String)
206
+ return condition
207
+ end
208
+
209
+ def convert_breakpoint_hit_condition(condition)
210
+ return nil if condition.nil? || condition.empty?
211
+ return nil unless condition.is_a?(String)
212
+
213
+ m = /^(?<op><|<=|=|==|===|=>|>|%)?\s*(?<value>[0-9]+)$/.match(condition)
214
+ raise InvalidRequestArgumentError.new("'#{condition}' is not a valid hit condition") unless m
215
+
216
+ v = m[:value].to_i
217
+ case m[:op]
218
+ when nil, '=', '==', '==='
219
+ return :eq, v
220
+
221
+ when '>'
222
+ return :ge, v - 1
223
+
224
+ when '>='
225
+ return :ge, v
226
+
227
+ when '%'
228
+ return :mod, v
229
+
230
+ else
231
+ raise InvalidRequestArgumentError.new("Byebug does not support hit conditions using '#{m[:op]}'") unless m
232
+ end
233
+ end
234
+
235
+ def find_or_add_breakpoint(verified, existing, source, pos)
236
+ if bp = verified.find { |bp| bp.source == source && bp.pos == pos }
237
+ return bp
238
+ end
239
+
240
+ if bp = existing.find { |bp| bp.source == source && bp.pos == pos }
241
+ existing.delete(bp)
242
+ else
243
+ bp = Byebug::Breakpoint.add(source, pos.is_a?(String) ? pos.to_sym : pos)
244
+ end
245
+
246
+ verified << bp
247
+ bp
248
+ end
249
+ end
250
+ end