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,17 @@
1
+ module Byebug
2
+ module DAP
3
+ module SafeHelpers
4
+ def safe(target, method, *args, &block)
5
+ if method.is_a?(Array) && args.empty?
6
+ method.each { |m| target = target.__send__(m) }
7
+ target
8
+ else
9
+ target.__send__(method, *args)
10
+ end
11
+ rescue StandardError => e
12
+ log "\n! #{e.message} (#{e.class})", *e.backtrace if Debug.evaluate
13
+ block.parameters.empty? ? yield : yield(e)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,19 @@
1
+ module Byebug::DAP
2
+ module Scalar
3
+ def ===(value)
4
+ case value
5
+ when nil, true, false
6
+ return true
7
+ when ::String, ::Symbol, ::Numeric
8
+ return true
9
+ when ::Time, ::Range
10
+ true
11
+ end
12
+
13
+ return true if defined?(::Date) && ::Date === value
14
+ return true if defined?(::DateTime) && ::DateTime === value
15
+
16
+ false
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,21 @@
1
+ module Byebug::DAP
2
+ class STDIO
3
+ extend Forwardable
4
+
5
+ def initialize
6
+ @in = STDIN.dup
7
+ @out = STDOUT.dup
8
+ @in.sync = true
9
+ @out.sync = true
10
+ end
11
+
12
+ def close; @in.close; @out.close; end
13
+ def flush; @in.flush; @out.flush; end
14
+ def fsync; @in.fsync; @out.fsync; end
15
+ def closed?; @in.closed? || @out.closed?; end
16
+
17
+ def_delegators :@in, :close_read, :bytes, :chars, :codepoints, :each, :each_byte, :each_char, :each_codepoint, :each_line, :getbyte, :getc, :gets, :lines, :pread, :print, :printf, :read, :read_nonblock, :readbyte, :readchar, :readline, :readlines, :readpartial, :sysread, :ungetbyte, :ungetc
18
+ def_delegators :@out, :<<, :close_write, :putc, :puts, :pwrite, :syswrite, :write, :write_nonblock
19
+ public :<<, :bytes, :chars, :close_read, :close_write, :codepoints, :each, :each_byte, :each_char, :each_codepoint, :each_line, :getbyte, :getc, :gets, :lines, :pread, :print, :printf, :putc, :puts, :pwrite, :read, :read_nonblock, :readbyte, :readchar, :readline, :readlines, :readpartial, :sysread, :syswrite, :ungetbyte, :ungetc, :write, :write_nonblock
20
+ end
21
+ end
@@ -0,0 +1,60 @@
1
+ module Byebug::DAP
2
+ module ValueHelpers
3
+ def prepare_value(val)
4
+ str = safe(val, :inspect) { safe(val, :to_s) { return yield } }
5
+ cls = safe(val, :class) { nil }
6
+ typ = safe(cls, :name) { safe(cls, :to_s) { nil } }
7
+
8
+ scalar = safe(-> { Scalar === val }, :call) { true }
9
+ return str, typ, [], [] if scalar
10
+
11
+ named = safe(val, :instance_variables) { [] }
12
+ named += safe(val, :class_variables) { [] }
13
+ # named += safe(val, :constants) { [] }
14
+
15
+ indexed = safe(-> {
16
+ return (0...val.size).to_a if val.is_a?(Array)
17
+ return val.keys if val.respond_to?(:keys) && val.respond_to?(:[])
18
+ []
19
+ }, :call) { [] }
20
+
21
+ return str, typ, named, indexed
22
+ end
23
+
24
+ def prepare_value_response(thnum, frnum, kind, name: nil, &block)
25
+ err = nil
26
+ raw = execute_on_thread(thnum, block) { |e| err = e; nil }
27
+
28
+ if err.nil?
29
+ value, type, named, indexed = prepare_value(raw) { |e| next exception_description(e), nil, [], [] }
30
+ else
31
+ type, named, indexed = nil, [], []
32
+ if err.is_a?(CommandProcessor::TimeoutError)
33
+ name = err.context.thread.name
34
+ value = "*Thread ##{err.context.thnum} #{name ? '(' + name + ')' : ''} unresponsive*"
35
+ else
36
+ value = exception_description(err)
37
+ end
38
+ end
39
+
40
+ case kind
41
+ when :variable
42
+ klazz = ::DAP::Variable
43
+ args = { name: safe(name, :to_s) { safe(name, :inspect) { '???' } }, value: value, type: type }
44
+ when :evaluate
45
+ klazz = ::DAP::EvaluateResponseBody
46
+ args = { result: value, type: type }
47
+ end
48
+
49
+ if named.empty? && indexed.empty?
50
+ args[:variablesReference] = 0
51
+ else
52
+ args[:variablesReference] = @session.save_variables(thnum, frnum, kind, raw, named, indexed)
53
+ args[:namedVariables] = named.size
54
+ args[:indexedVariables] = indexed.size
55
+ end
56
+
57
+ klazz.new(args).validate!
58
+ end
59
+ end
60
+ end
@@ -1,30 +1,13 @@
1
1
  module Byebug
2
2
  module DAP
3
3
  class Server
4
- class STDIO
5
- extend Forwardable
6
-
7
- def initialize
8
- @in = STDIN
9
- @out = STDOUT
10
- STDIN.sync = true
11
- STDOUT.sync = true
12
- end
13
-
14
- def close; @in.close; @out.close; end
15
- def flush; @in.flush; @out.flush; end
16
- def fsync; @in.fsync; @out.fsync; end
17
-
18
- def_delegators :@in, :close_read, :bytes, :chars, :codepoints, :each, :each_byte, :each_char, :each_codepoint, :each_line, :getbyte, :getc, :gets, :lines, :pread, :print, :printf, :read, :read_nonblock, :readbyte, :readchar, :readline, :readlines, :readpartial, :sysread, :ungetbyte, :ungetc
19
- def_delegators :@out, :<<, :close_write, :putc, :puts, :pwrite, :syswrite, :write, :write_nonblock
20
- public :<<, :bytes, :chars, :close_read, :close_write, :codepoints, :each, :each_byte, :each_char, :each_codepoint, :each_line, :getbyte, :getc, :gets, :lines, :pread, :print, :printf, :putc, :puts, :pwrite, :read, :read_nonblock, :readbyte, :readchar, :readline, :readlines, :readpartial, :sysread, :syswrite, :ungetbyte, :ungetc, :write, :write_nonblock
21
- end
22
-
23
- def initialize
4
+ def initialize(capture: true, forward: true)
24
5
  @started = false
25
6
  @mu = Mutex.new
26
7
  @cond = ConditionVariable.new
27
8
  @configured = false
9
+ @capture = capture
10
+ @forward = forward
28
11
  end
29
12
 
30
13
  def start(host, port = 0)
@@ -42,21 +25,26 @@ module Byebug
42
25
  return if @started
43
26
  @started = true
44
27
 
45
- launch TCPServer.new(host, port)
28
+ @ios = CapturedIO.new(@forward, @forward) if @capture
29
+ launch_accept TCPServer.new(host, port)
46
30
  end
47
31
 
48
32
  def start_unix(socket)
49
33
  return if @started
50
34
  @started = true
51
35
 
52
- launch UNIXServer.new(socket)
36
+ @ios = CapturedIO.new(@forward, @forward) if @capture
37
+ launch_accept UNIXServer.new(socket)
53
38
  end
54
39
 
55
40
  def start_stdio
56
41
  return if @started
57
42
  @started = true
58
43
 
59
- launch STDIO.new
44
+ stream = STDIO.new
45
+ STDIN.close
46
+ @ios = CapturedIO.new(false, @forward) if @capture
47
+ launch stream
60
48
  end
61
49
 
62
50
  def wait_for_client
@@ -71,30 +59,58 @@ module Byebug
71
59
 
72
60
  private
73
61
 
74
- def launch(server)
62
+ def log
63
+ if @ios
64
+ @ios.log
65
+ elsif defined?(LOG)
66
+ LOG
67
+ else
68
+ STDERR
69
+ end
70
+ end
71
+
72
+ def launch(stream)
75
73
  DebugThread.new do
76
- if server.respond_to?(:accept)
77
- while session = server.accept
78
- debug session
79
- end
80
- else
81
- debug server
82
- end
74
+ debug stream
75
+
76
+ ensure
77
+ @ios&.restore
83
78
  end
84
79
 
85
80
  self
86
81
  end
87
82
 
88
- def debug(session)
89
- Context.interface = Byebug::DAP::Interface.new(session)
90
- Context.processor = Byebug::DAP::CommandProcessor
83
+ def launch_accept(server)
84
+ DebugThread.new do
85
+ while socket = server.accept
86
+ debug socket
87
+ end
88
+
89
+ ensure
90
+ @ios&.restore
91
+ end
92
+
93
+ self
94
+ end
91
95
 
92
- Byebug::DAP::Controller.new(Context.interface) do
96
+ def debug(connection)
97
+ session = Byebug::DAP::Session.new(connection, @ios) do
93
98
  @mu.synchronize do
94
99
  @configured = true
95
100
  @cond.broadcast
96
101
  end
97
- end.run
102
+ end
103
+
104
+ session.execute
105
+
106
+ rescue IOError, Errno::EPIPE, Errno::ECONNRESET, Errno::ECONNABORTED
107
+ log.puts "Client disconnected"
108
+
109
+ rescue StandardError => e
110
+ log.puts "#{e.message} (#{e.class})", *e.backtrace
111
+
112
+ ensure
113
+ session.stop!
98
114
  end
99
115
  end
100
116
  end
@@ -0,0 +1,176 @@
1
+ module Byebug
2
+ module DAP
3
+ class Session
4
+ include SafeHelpers
5
+
6
+ @@children = []
7
+
8
+ def self.child_spawned(name, pid, socket)
9
+ child = ChildSpawnedEventBody.new(name: name, pid: pid, socket: socket)
10
+ @@children << child
11
+
12
+ session = Context.interface
13
+ session.event! child if session.is_a?(Byebug::DAP::Session)
14
+
15
+ return true
16
+
17
+ rescue IOError, Errno::EPIPE, Errno::ECONNRESET, Errno::ECONNABORTED
18
+ return false
19
+ end
20
+
21
+ def initialize(connection, ios, &block)
22
+ @connection = connection
23
+ @ios = ios
24
+ @on_configured = block
25
+ @pid = Process.pid
26
+ @log_points = {}
27
+ @frame_ids = Handles.new
28
+ @variable_refs = Handles.new
29
+ @trace = TracePoint.new(:thread_begin, :thread_end) { |t| process_trace t }
30
+
31
+ notify_of_children
32
+ end
33
+
34
+ def log(*args)
35
+ logger =
36
+ if @ios
37
+ @ios.log
38
+ elsif defined?(LOG)
39
+ LOG
40
+ else
41
+ STDERR
42
+ end
43
+ logger.puts(*args)
44
+ end
45
+
46
+ def execute
47
+ Context.interface = self
48
+ Context.processor = Byebug::DAP::CommandProcessor
49
+
50
+ Command.execute(self, receive) until @connection.closed?
51
+
52
+ Context.interface = LocalInterface.new
53
+ end
54
+
55
+ def invalidate_handles!
56
+ @frame_ids.clear!
57
+ @variable_refs.clear!
58
+ end
59
+
60
+ def start!(mode)
61
+ @trace.enable
62
+ Byebug.mode = mode
63
+ Byebug.start
64
+ @exit_on_stop = true if mode == :launched
65
+ end
66
+
67
+ def configured!
68
+ return unless @on_configured
69
+
70
+ callback, @on_configured = @on_configured, callback
71
+ callback.call
72
+ end
73
+
74
+ def stop!
75
+ exit if @exit_on_stop && @pid == Process.pid
76
+
77
+ Byebug.mode = :off
78
+ Byebug.stop
79
+ @trace.disable
80
+ @connection.close
81
+ end
82
+
83
+ def event!(event, **values)
84
+ if (cls = event.class.name.split('::').last) && cls.end_with?('EventBody')
85
+ body, event = event, cls[0].downcase + cls[1...-9]
86
+
87
+ elsif event.is_a?(String) && !values.empty?
88
+ body = ::DAP.const_get("#{event[0].upcase}#{event[1..]}EventBody").new(values)
89
+ end
90
+
91
+ send ::DAP::Event.new(event: event, body: body)
92
+ end
93
+
94
+ def respond!(request, body = nil, success: true, message: 'Success', **values)
95
+ send ::DAP::Response.new(
96
+ request_seq: request.seq,
97
+ command: request.command,
98
+ success: success,
99
+ message: message,
100
+ body: body,
101
+ **values)
102
+ end
103
+
104
+ def save_variables(*args)
105
+ @variable_refs << args
106
+ end
107
+
108
+ def restore_variables(ref)
109
+ @variable_refs[ref]
110
+ end
111
+
112
+ def save_frame(*args)
113
+ @frame_ids << args
114
+ end
115
+
116
+ def restore_frame(id)
117
+ @frame_ids[id]
118
+ end
119
+
120
+ def get_log_point(breakpoint)
121
+ @log_points[breakpoint.id]
122
+ end
123
+
124
+ def set_log_point(breakpoint, expr)
125
+ if expr.nil? || expr.empty?
126
+ @log_points.delete(breakpoint.id)
127
+ else
128
+ @log_points[breakpoint.id] = expr
129
+ end
130
+ end
131
+
132
+ def clear_breakpoints(*breakpoints)
133
+ breakpoints.each do |breakpoint|
134
+ Byebug.breakpoints.delete(breakpoint)
135
+ @log_points.delete(breakpoint.id)
136
+ end
137
+ end
138
+
139
+ private
140
+
141
+ def notify_of_children
142
+ @@children.each { |c| event! c }
143
+ rescue IOError, Errno::EPIPE, Errno::ECONNRESET, Errno::ECONNABORTED
144
+ # client is closed
145
+ end
146
+
147
+ def send(message)
148
+ log "#{Process.pid} > #{message.to_wire}" if Debug.protocol
149
+ message.validate!
150
+ @connection.write ::DAP::Encoding.encode(message)
151
+ end
152
+
153
+ def receive
154
+ m = ::DAP::Encoding.decode(@connection)
155
+ log "#{Process.pid} < #{m.to_wire}" if Debug.protocol
156
+ m
157
+ end
158
+
159
+ def process_trace(trace)
160
+ return unless Byebug.started?
161
+
162
+ ctx = Byebug.contexts.find { |c| c.thread == Thread.current }
163
+
164
+ case trace.event
165
+ when :thread_begin
166
+ event! 'thread', reason: 'started', threadId: ctx.thnum
167
+ when :thread_end
168
+ event! 'thread', reason: 'exited', threadId: ctx.thnum
169
+ end
170
+
171
+ rescue IOError, Errno::EPIPE, Errno::ECONNRESET, Errno::ECONNABORTED
172
+ # client disconnected, ignore error
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,11 @@
1
+ module Byebug
2
+ module DAP
3
+ NAME = 'byebug-dap'
4
+ VERSION = '0.1.3'
5
+ SUMMARY = 'Debug Adapter Protocol for Byebug'
6
+ DESCRIPTION = 'Implements a Debug Adapter Protocol interface for Byebug'
7
+ AUTHORS = ['Ethan Reesor']
8
+ WEBSITE = 'https://gitlab.com/firelizzard/byebug-dap'
9
+ LICENSE = 'Apache-2.0'
10
+ end
11
+ end