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
@@ -0,0 +1,48 @@
1
+ module Byebug::DAP
2
+ class Command::SetBreakpoints < Command
3
+ # "Sets multiple breakpoints for a single source and clears all previous breakpoints in that source.
4
+ # "To clear all breakpoint for a source, specify an empty array.
5
+ # "When a breakpoint is hit, a ‘stopped’ event (with reason ‘breakpoint’) is generated.
6
+
7
+ register!
8
+
9
+ def execute
10
+ return unless path = can_read_file!(args.source.path)
11
+ if args.lines.empty? && args.breakpoints.empty?
12
+ Byebug.breakpoints.reject! { |bp| bp.source == path }
13
+ respond! body: { breakpoints: [] }
14
+ return
15
+ end
16
+
17
+ existing = Byebug.breakpoints.filter { |bp| bp.source == path }
18
+ verified = []
19
+ lines = potential_breakpoint_lines(path) { |e|
20
+ respond! success: false, message: "Failed to resolve breakpoints for #{path}"
21
+ return
22
+ }
23
+
24
+ (args.lines & lines).each do |l|
25
+ find_or_add_breakpoint(verified, existing, path, l)
26
+ end
27
+
28
+ args.breakpoints.filter { |rq| lines.include?(rq.line) }.each do |rq|
29
+ bp = find_or_add_breakpoint(verified, existing, path, rq.line)
30
+ bp.expr = convert_breakpoint_condition(rq.condition)
31
+ bp.hit_condition, bp.hit_value = convert_breakpoint_hit_condition(rq.hitCondition)
32
+ @session.set_log_point(bp, rq.logMessage)
33
+ end
34
+
35
+ @session.clear_breakpoints(*existing)
36
+
37
+ respond! body: {
38
+ breakpoints: verified.map { |bp|
39
+ {
40
+ id: bp.id,
41
+ line: bp.pos,
42
+ verified: true,
43
+ }
44
+ }
45
+ }
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,28 @@
1
+ module Byebug::DAP
2
+ class Command::SetExceptionBreakpoints < Command
3
+ # "The request configures the debuggers response to thrown exceptions.
4
+ # "If an exception is configured to break, a ‘stopped’ event is fired (with reason ‘exception’).
5
+
6
+ FILTERS = [
7
+ {
8
+ filter: 'all',
9
+ label: 'Exceptions',
10
+ },
11
+ ]
12
+
13
+ register!
14
+
15
+ def execute
16
+ Byebug.catchpoints.clear
17
+
18
+ args.filters.each do |f|
19
+ case f
20
+ when 'all'
21
+ Byebug.add_catchpoint('Exception')
22
+ end
23
+ end
24
+
25
+ respond!
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,96 @@
1
+ module Byebug::DAP
2
+ class Command::SetFunctionBreakpoints < Command
3
+ # "Replaces all existing function breakpoints with new function breakpoints.
4
+ # "To clear all function breakpoints, specify an empty array.
5
+ # "When a function breakpoint is hit, a ‘stopped’ event (with reason ‘function breakpoint’) is generated.
6
+
7
+ register!
8
+
9
+ def execute
10
+ ::Byebug.breakpoints.each { |bp| ::Byebug::Breakpoint.remove(bp.id) if bp.pos.is_a?(String) }
11
+
12
+ existing = Byebug.breakpoints.filter { |bp| bp.pos.is_a?(String) }
13
+ verified = []
14
+ results = []
15
+
16
+ args.breakpoints.each do |rq|
17
+ m = /^(?<class>[:\w]+)(?<sep>\.|#)(?<method>\w+)$/.match(rq.name)
18
+ unless m
19
+ results << {
20
+ verified: false,
21
+ message: "'#{rq.name}' is not a valid method identifier",
22
+ }
23
+ next
24
+ end
25
+
26
+ bp = find_or_add_breakpoint(verified, existing, m[:class], m[:method])
27
+ bp.expr = convert_breakpoint_condition(rq.condition)
28
+ bp.hit_condition, bp.hit_value = convert_breakpoint_hit_condition(rq.hitCondition)
29
+ end
30
+
31
+ verified.each do |bp|
32
+ cm, im = resolve_method(bp.source, bp.pos)
33
+
34
+ if cm.nil? && im.nil?
35
+ results << {
36
+ id: bp.id,
37
+ verified: true
38
+ }
39
+ end
40
+
41
+ unless cm.nil?
42
+ results << {
43
+ id: bp.id,
44
+ verified: true,
45
+ source: ::DAP::Source.new(name: File.basename(cm[0]), path: cm[0]),
46
+ line: cm[1]
47
+ }
48
+ end
49
+
50
+ unless im.nil?
51
+ results << {
52
+ id: bp.id,
53
+ verified: true,
54
+ source: ::DAP::Source.new(name: File.basename(im[0]), path: im[0]),
55
+ line: im[1]
56
+ }
57
+ end
58
+ end
59
+
60
+ @session.clear_breakpoints(*existing)
61
+
62
+ respond! body: { breakpoints: results }
63
+ end
64
+
65
+ private
66
+
67
+ def resolve_method(class_name, method_name)
68
+ scope = Object
69
+ class_name.split('::').each do |n|
70
+ scope = scope.const_get(n)
71
+ rescue NameError
72
+ return nil
73
+ end
74
+
75
+ class_method =
76
+ begin
77
+ scope.method(method_name)&.source_location
78
+ rescue NameError
79
+ nil
80
+ end
81
+
82
+ instance_method =
83
+ begin
84
+ scope.instance_method(method_name)&.source_location
85
+ rescue NameError
86
+ nil
87
+ end
88
+
89
+ return class_method, instance_method
90
+
91
+ rescue StandardError => e
92
+ LOG.puts "#{e.message} (#{e.class.name})", *e.backtrace if Debug.evaluate
93
+ nil
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,12 @@
1
+ module Byebug::DAP
2
+ class Command::Source < Command
3
+ # "The request retrieves the source code for a given source reference.
4
+
5
+ register!
6
+
7
+ def execute
8
+ return unless path = can_read_file!(args.source.path)
9
+ respond! body: { content: IO.read(path) }
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,50 @@
1
+ module Byebug::DAP
2
+ class Command::StackTrace < Command
3
+ # "The request returns a stacktrace from the current execution state.
4
+
5
+ register!
6
+
7
+ def execute
8
+ started!
9
+
10
+ ctx = find_thread(args.threadId)
11
+
12
+ first = args.startFrame || 0
13
+ if !args.levels
14
+ last = ctx.stack_size
15
+ else
16
+ last = first + args.levels
17
+ if last > ctx.stack_size
18
+ last = ctx.stack_size
19
+ end
20
+ end
21
+
22
+ frames = (first...last).map do |i|
23
+ frame = ::Byebug::Frame.new(ctx, i)
24
+ {
25
+ id: @session.save_frame(ctx.thnum, i),
26
+ name: frame_name(frame),
27
+ source: {
28
+ name: File.basename(frame.file),
29
+ path: File.expand_path(frame.file),
30
+ },
31
+ line: frame.line,
32
+ column: 1,
33
+ }
34
+ end
35
+
36
+ respond! body: {
37
+ stackFrames: frames,
38
+ totalFrames: ctx.stack_size,
39
+ }
40
+ end
41
+
42
+ private
43
+
44
+ def frame_name(frame)
45
+ frame.deco_call
46
+ rescue
47
+ frame.deco_block + frame.deco_class + frame.deco_method + "(?)"
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,24 @@
1
+ module Byebug::DAP
2
+ class Command::StepIn < ContextualCommand
3
+ # "The request starts the debuggee to step into a function/method if possible.
4
+ # "If it cannot step into a target, ‘stepIn’ behaves like ‘next’.
5
+ # "The debug adapter first sends the response and then a ‘stopped’ event (with reason ‘step’) after the step has completed.
6
+ # "If there are multiple function/method calls (or other targets) on the source line,
7
+ # "the optional argument ‘targetId’ can be used to control into which target the ‘stepIn’ should occur.
8
+ # "The list of possible targets for a given source line can be retrieved via the ‘stepInTargets’ request.
9
+
10
+ register!
11
+
12
+ def execute_in_context
13
+ @context.step_into(1, @context.frame.pos)
14
+ :stop
15
+ end
16
+
17
+ private
18
+
19
+ def forward_to_context(ctx)
20
+ super
21
+ respond!
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,21 @@
1
+ module Byebug::DAP
2
+ class Command::StepOut < ContextualCommand
3
+ # "The request starts the debuggee to run again for one step.
4
+ # "The debug adapter first sends the response and then a ‘stopped’ event (with reason ‘step’) after the step has completed.
5
+
6
+ register!
7
+
8
+ def execute_in_context
9
+ @context.step_out(@context.frame.pos + 1, false)
10
+ @context.frame = 0
11
+ :stop
12
+ end
13
+
14
+ private
15
+
16
+ def forward_to_context(ctx)
17
+ super
18
+ respond!
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,20 @@
1
+ module Byebug::DAP
2
+ class Command::Threads < Command
3
+ # "The request retrieves a list of all threads.
4
+
5
+ register!
6
+
7
+ def execute
8
+ started!
9
+
10
+ respond! body: ::DAP::ThreadsResponseBody.new(
11
+ threads: Byebug
12
+ .contexts
13
+ .filter { |ctx| !ctx.thread.is_a?(::Byebug::DebugThread) }
14
+ .map { |ctx| ::DAP::Thread.new(
15
+ id: ctx.thnum,
16
+ name: ctx.thread.name || "Thread ##{ctx.thnum}"
17
+ ).validate! })
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,33 @@
1
+ module Byebug::DAP
2
+ class Command::Variables < Command
3
+ # "Retrieves all child variables for the given variable reference.
4
+ # "An optional filter can be used to limit the fetched children to either named or indexed children
5
+
6
+ include ValueHelpers
7
+
8
+ register!
9
+
10
+ def execute
11
+ started!
12
+
13
+ thnum, frnum, named, indexed = resolve_variables_reference(args.variablesReference)
14
+
15
+ case args.filter
16
+ when 'named'
17
+ indexed = []
18
+ when 'indexed'
19
+ named = []
20
+ end
21
+
22
+ vars = named + indexed
23
+
24
+ first = args.start || 0
25
+ last = args.count ? first + args.count : vars.size
26
+ last = vars.size unless last < vars.size
27
+
28
+ variables = vars[first...last].map { |var, get| prepare_value_response(thnum, frnum, :variable, name: var) { get.call(var) } }
29
+
30
+ respond! body: ::DAP::VariablesResponseBody.new(variables: variables)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,30 @@
1
+ module Byebug::DAP
2
+ class ContextualCommand < Command
3
+ def self.resolve!(session, request)
4
+ return unless cls = super
5
+ return cls if cls < ContextualCommand
6
+
7
+ raise "Not a contextual command: #{command}"
8
+ end
9
+
10
+ def initialize(session, request, processor = nil)
11
+ super(session, request)
12
+ @processor = processor
13
+ @context = processor&.context
14
+ end
15
+
16
+ def execute
17
+ return execute_in_context if @processor
18
+
19
+ started!
20
+
21
+ forward_to_context find_thread(args.threadId)
22
+ end
23
+
24
+ private
25
+
26
+ def forward_to_context(ctx)
27
+ ctx.processor << @request
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,65 @@
1
+ module Byebug::DAP
2
+ class CapturedIO
3
+ def initialize(forward_stdout, forward_stderr)
4
+ @forward_stdout = forward_stdout
5
+ @forward_stderr = forward_stderr
6
+ @stdout = CapturedOutput.new STDOUT
7
+ @stderr = CapturedOutput.new STDERR
8
+ @stop = false
9
+
10
+ Byebug::DebugThread.new { capture }
11
+ end
12
+
13
+ def log
14
+ if defined?(LOG)
15
+ LOG
16
+ elsif @stderr
17
+ @stderr.original
18
+ else
19
+ STDERR
20
+ end
21
+ end
22
+
23
+ def restore
24
+ @stop = true
25
+ @stdout.restore
26
+ @stderr.restore
27
+ end
28
+
29
+ private
30
+
31
+ def capture
32
+ until @stop do
33
+ r, = IO.select([@stdout.captured, @stderr.captured])
34
+
35
+ r.each do |r|
36
+ case r
37
+ when @stdout.captured
38
+ b = @stdout.captured.read_nonblock(1024)
39
+ @stdout.original.write(b) if @forward_stdout
40
+ send(:stdout, b)
41
+
42
+ when @stderr.captured
43
+ b = @stderr.captured.read_nonblock(1024)
44
+ @stderr.original.write(b) if @forward_stderr
45
+ send(:stderr, b)
46
+ end
47
+ end
48
+ end
49
+
50
+ rescue EOFError, Errno::EBADF
51
+ rescue StandardError => e
52
+ log.puts "#{e.message} (#{e.class})", *e.backtrace
53
+ end
54
+
55
+ def send(source, data)
56
+ session = Byebug::Context.interface
57
+ return unless session.is_a?(Session)
58
+
59
+ session.event! 'output', category: source.to_s, output: data
60
+
61
+ rescue IOError, Errno::EPIPE, Errno::ECONNRESET, Errno::ECONNABORTED
62
+ # client disconnected
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,21 @@
1
+ module Byebug::DAP
2
+ class CapturedOutput
3
+ attr_reader :original, :captured
4
+
5
+ def initialize(io)
6
+ @io = io
7
+ @original = io.dup
8
+ @captured, pw = IO.pipe
9
+
10
+ io.reopen(pw)
11
+ pw.close
12
+ end
13
+
14
+ def restore
15
+ @io.reopen(@original)
16
+ @original.close
17
+ @captured.close
18
+ return nil
19
+ end
20
+ end
21
+ end