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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +14 -3
- data/README.md +5 -5
- data/bin/byebug-dap +25 -8
- data/lib/byebug/dap.rb +48 -18
- data/lib/byebug/dap/command.rb +250 -0
- data/lib/byebug/dap/command_processor.rb +50 -49
- data/lib/byebug/dap/commands/attach.rb +13 -0
- data/lib/byebug/dap/commands/breakpoint_locations.rb +28 -0
- data/lib/byebug/dap/commands/configuration_done.rb +12 -0
- data/lib/byebug/dap/commands/continue.rb +18 -0
- data/lib/byebug/dap/commands/disconnect.rb +16 -0
- data/lib/byebug/dap/commands/evaluate.rb +27 -0
- data/lib/byebug/dap/commands/exception_info.rb +40 -0
- data/lib/byebug/dap/commands/initialize.rb +27 -0
- data/lib/byebug/dap/commands/launch.rb +13 -0
- data/lib/byebug/dap/commands/next.rb +20 -0
- data/lib/byebug/dap/commands/pause.rb +20 -0
- data/lib/byebug/dap/commands/scopes.rb +56 -0
- data/lib/byebug/dap/commands/set_breakpoints.rb +48 -0
- data/lib/byebug/dap/commands/set_exception_breakpoints.rb +28 -0
- data/lib/byebug/dap/commands/set_function_breakpoints.rb +96 -0
- data/lib/byebug/dap/commands/source.rb +12 -0
- data/lib/byebug/dap/commands/stack_trace.rb +50 -0
- data/lib/byebug/dap/commands/step_in.rb +24 -0
- data/lib/byebug/dap/commands/step_out.rb +21 -0
- data/lib/byebug/dap/commands/threads.rb +20 -0
- data/lib/byebug/dap/commands/variables.rb +33 -0
- data/lib/byebug/dap/contextual_command.rb +30 -0
- data/lib/byebug/dap/helpers/captured_io.rb +65 -0
- data/lib/byebug/dap/helpers/captured_output.rb +21 -0
- data/lib/byebug/dap/{channel.rb → helpers/channel.rb} +0 -0
- data/lib/byebug/dap/{child_spawned_event_body.rb → helpers/child_spawned_event_body.rb} +0 -0
- data/lib/byebug/dap/{handles.rb → helpers/handles.rb} +0 -0
- data/lib/byebug/dap/{invalid_request_argument_error.rb → helpers/invalid_request_argument_error.rb} +0 -0
- data/lib/byebug/dap/helpers/safe_helpers.rb +17 -0
- data/lib/byebug/dap/helpers/scalar.rb +19 -0
- data/lib/byebug/dap/helpers/stdio.rb +21 -0
- data/lib/byebug/dap/helpers/value_helpers.rb +60 -0
- data/lib/byebug/dap/server.rb +52 -36
- data/lib/byebug/dap/session.rb +176 -0
- data/lib/byebug/gem.rb +11 -0
- metadata +38 -10
- data/lib/byebug/dap/controller.rb +0 -252
- data/lib/byebug/dap/interface.rb +0 -303
- 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
|