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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 18d52dd5f136afc3350836a23abe3c9b957646358a566e15bfac0aedd070189c
|
4
|
+
data.tar.gz: 615c83e3512cc884f1d9dd2661b578cdf4314ebe2f3bab55f4c76f70a7fb17fc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f5932e26ae7fe7f9518e35df4a22aad7cb363f57d56680e8416e2f6dfa3aed447b7a38270e752313b08a7244108a13f64daee74b58d715ce333b95e4230abbf8
|
7
|
+
data.tar.gz: 4e150cd4ba325258f41d91496ab6d6914294d15330a124c1501ec50b28991a8619d49be3420d4f22305c5067c5d42de167a62dbf2a919b501af9dcb75c18f6f1
|
data/CHANGELOG.md
CHANGED
@@ -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 `
|
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
|
-
-
|
9
|
-
-
|
10
|
-
-
|
11
|
-
-
|
12
|
-
|
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).
|
data/bin/byebug-dap
CHANGED
@@ -11,14 +11,20 @@ def next_arg
|
|
11
11
|
arg = ARGV.pop
|
12
12
|
return arg if arg
|
13
13
|
|
14
|
-
|
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
|
-
|
70
|
+
LOG.puts USAGE, "One of --stdio, --listen, or --unix is required"
|
59
71
|
exit!
|
60
72
|
end
|
61
73
|
|
62
74
|
begin
|
63
|
-
|
75
|
+
LOG.print "Starting DAP... " unless options[:start_code]
|
64
76
|
|
65
|
-
server = Byebug.
|
77
|
+
server = Byebug::DAP::Server.new(capture: options[:capture_output], forward: !options[:suppress_output])
|
78
|
+
server.start(host, port)
|
66
79
|
|
67
|
-
|
68
|
-
STDERR.flush
|
80
|
+
LOG.puts options[:start_code] if options[:start_code]
|
69
81
|
|
70
82
|
if options[:wait]
|
71
|
-
|
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
|
-
|
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
|
data/lib/byebug/dap.rb
CHANGED
@@ -3,32 +3,62 @@ require 'byebug'
|
|
3
3
|
require 'byebug/core'
|
4
4
|
require 'byebug/remote'
|
5
5
|
|
6
|
-
require_relative '
|
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
|
-
|
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/
|
15
|
-
require_relative 'dap/
|
20
|
+
require_relative 'dap/session'
|
21
|
+
require_relative 'dap/server'
|
16
22
|
|
17
23
|
module Byebug
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
31
|
-
|
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
|