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
@@ -12,16 +12,21 @@ module Byebug
12
12
  end
13
13
  end
14
14
 
15
- attr_reader :context, :interface
15
+ attr_reader :context, :last_exception
16
+ attr_writer :pause_requested
16
17
 
17
- def initialize(context, interface)
18
+ def initialize(context, session)
18
19
  @context = context
19
- @interface = interface
20
+ @session = session
20
21
  @requests = Channel.new
21
22
  @exec_mu = Mutex.new
22
23
  @exec_ch = Channel.new
23
24
  end
24
25
 
26
+ def log(*args)
27
+ @session.log(*args)
28
+ end
29
+
25
30
  def <<(message)
26
31
  @requests.push(message, timeout: 1) { raise TimeoutError.new(context) }
27
32
  end
@@ -54,67 +59,61 @@ module Byebug
54
59
  next
55
60
  end
56
61
 
57
- case request.command
58
- when 'continue'
59
- # "The request starts the debuggee to run again.
60
-
61
- break
62
-
63
- when 'pause'
64
- # "The request suspends the debuggee.
65
- # "The debug adapter first sends the response and then a ‘stopped’ event (with reason ‘pause’) after the thread has been paused successfully.
62
+ break if ContextualCommand.execute(@session, request, self) == :stop
63
+ end
66
64
 
67
- @pause_requested = true
65
+ @last_exception = nil
66
+ @session.invalidate_handles!
68
67
 
69
- when 'next'
70
- # "The request starts the debuggee to run again for one step.
71
- # "The debug adapter first sends the response and then a ‘stopped’ event (with reason ‘step’) after the step has completed.
68
+ rescue StandardError => e
69
+ log "\n! #{e.message} (#{e.class})", *e.backtrace
70
+ end
72
71
 
73
- context.step_over(1, context.frame.pos)
74
- break
72
+ def logpoint!
73
+ return false unless @last_breakpoint
75
74
 
76
- when 'stepIn'
77
- # "The request starts the debuggee to step into a function/method if possible.
78
- # "If it cannot step into a target, ‘stepIn’ behaves like ‘next’.
79
- # "The debug adapter first sends the response and then a ‘stopped’ event (with reason ‘step’) after the step has completed.
80
- # "If there are multiple function/method calls (or other targets) on the source line,
81
- # "the optional argument ‘targetId’ can be used to control into which target the ‘stepIn’ should occur.
82
- # "The list of possible targets for a given source line can be retrieved via the ‘stepInTargets’ request.
75
+ breakpoint, @last_breakpoint = @last_breakpoint, nil
76
+ expr = @session.get_log_point(breakpoint)
77
+ return false unless expr
83
78
 
84
- context.step_into(1, context.frame.pos)
85
- break
79
+ binding = @context.frame._binding
80
+ msg = expr.gsub(/\{([^\}]+)\}/) do |x|
81
+ safe(binding, :eval, x[1...-1]) { return true } # ignore bad log points
82
+ end
86
83
 
87
- when 'stepOut'
88
- # "The request starts the debuggee to run again for one step.
89
- # "The debug adapter first sends the response and then a ‘stopped’ event (with reason ‘step’) after the step has completed.
84
+ body = {
85
+ category: 'console',
86
+ output: msg + "\n",
87
+ }
90
88
 
91
- context.step_out(context.frame.pos + 1, false)
92
- context.frame = 0
93
- break
94
- end
89
+ if breakpoint.pos.is_a?(Integer)
90
+ body[:line] = breakpoint.pos
91
+ body[:source] = {
92
+ name: File.basename(breakpoint.source),
93
+ path: breakpoint.source,
94
+ }
95
95
  end
96
96
 
97
- interface.invalidate_handles!
98
-
99
- rescue StandardError => e
100
- STDERR.puts "\n! #{e.message} (#{e.class})", *e.backtrace
97
+ @session.event! 'output', **body
98
+ return true
101
99
  end
102
100
 
103
101
  def stopped!
102
+ return if logpoint!
103
+
104
104
  case context.stop_reason
105
105
  when :breakpoint
106
106
  args = {
107
107
  reason: 'breakpoint',
108
108
  description: 'Hit breakpoint',
109
- text: "Stopped by breakpoint at #{context.frame.file}:#{context.frame.line}",
109
+ text: "Hit breakpoint at #{context.location}",
110
110
  }
111
111
 
112
112
  when :catchpoint
113
- # TODO this is probably not the right message
114
113
  args = {
115
- reason: 'catchpoint',
114
+ reason: 'exception',
116
115
  description: 'Hit catchpoint',
117
- text: "Stopped by catchpoint at #{context.location}: `#{@at_catchpoint}'",
116
+ text: "Hit catchpoint at #{context.location}",
118
117
  }
119
118
 
120
119
  when :step
@@ -122,20 +121,22 @@ module Byebug
122
121
  @pause_requested = false
123
122
  args = {
124
123
  reason: 'pause',
125
- text: "Paused at #{context.frame.file}:#{context.frame.line}"
124
+ description: 'Paused',
125
+ text: "Paused at #{context.location}"
126
126
  }
127
127
  else
128
128
  args = {
129
129
  reason: 'step',
130
- text: "Stepped at #{context.frame.file}:#{context.frame.line}"
130
+ description: 'Stepped',
131
+ text: "Stepped at #{context.location}"
131
132
  }
132
133
  end
133
134
 
134
135
  else
135
- STDERR.puts "Stopped for unknown reason: #{context.stop_reason}"
136
+ log "Stopped for unknown reason: #{context.stop_reason}"
136
137
  end
137
138
 
138
- interface.event! 'stopped', threadId: context.thnum, **args if args
139
+ @session.event! 'stopped', threadId: context.thnum, **args if args
139
140
 
140
141
  process_requests
141
142
  end
@@ -153,17 +154,17 @@ module Byebug
153
154
  end
154
155
 
155
156
  # def at_tracing
156
- # interface.puts "Tracing: #{context.full_location}"
157
+ # @session.puts "Tracing: #{context.full_location}"
157
158
 
158
159
  # # run_auto_cmds(2)
159
160
  # end
160
161
 
161
162
  def at_breakpoint(breakpoint)
162
- @at_breakpoint = breakpoint
163
+ @last_breakpoint = breakpoint
163
164
  end
164
165
 
165
166
  def at_catchpoint(exception)
166
- @at_catchpoint = exception
167
+ @last_exception = exception
167
168
  end
168
169
  end
169
170
  end
@@ -0,0 +1,13 @@
1
+ module Byebug::DAP
2
+ class Command::Attach < Command
3
+ # "The attach request is sent from the client to the debug adapter to attach to a debuggee that is already running.
4
+
5
+ register!
6
+
7
+ def execute
8
+ stopped!
9
+ @session.start!(:attached)
10
+ respond!
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,28 @@
1
+ module Byebug::DAP
2
+ class Command::BreakpointLocations < Command
3
+ # "The ‘breakpointLocations’ request returns all possible locations for source breakpoints in a given range.
4
+
5
+ register!
6
+
7
+ def execute
8
+ return unless path = can_read_file!(args.source.path)
9
+ lines = potential_breakpoint_lines(path) { |e|
10
+ respond! success: false, message: "Failed to resolve breakpoints for #{path}"
11
+ return
12
+ }
13
+
14
+ unless args.endLine
15
+ if lines.include?(args.line)
16
+ respond! body: { breakpoints: [{ line: args.line }] }
17
+ else
18
+ respond! body: { breakpoints: [] }
19
+ end
20
+ return
21
+ end
22
+
23
+ range = [args.line..args.endLine]
24
+ lines.filter! { |l| range.include?(l) }
25
+ respond! body: { breakpoints: lines.map { |l| { line: l } } }
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,12 @@
1
+ module Byebug::DAP
2
+ class Command::ConfigurationDone < Command
3
+ # "This optional request indicates that the client has finished initialization of the debug adapter.
4
+
5
+ register!
6
+
7
+ def execute
8
+ respond!
9
+ @session.configured!
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,18 @@
1
+ module Byebug::DAP
2
+ class Command::Continue < ContextualCommand
3
+ # "The request starts the debuggee to run again.
4
+
5
+ register!
6
+
7
+ def execute_in_context
8
+ :stop
9
+ end
10
+
11
+ private
12
+
13
+ def forward_to_context(ctx)
14
+ super
15
+ respond!
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,16 @@
1
+ module Byebug::DAP
2
+ class Command::Disconnect < Command
3
+ # "The ‘disconnect’ request is sent from the client to the debug adapter in order to stop debugging.
4
+ # "It asks the debug adapter to disconnect from the debuggee and to terminate the debug adapter.
5
+ # "If the debuggee has been started with the ‘launch’ request, the ‘disconnect’ request terminates the debuggee.
6
+ # "If the ‘attach’ request was used to connect to the debuggee, ‘disconnect’ does not terminate the debuggee.
7
+ # "This behavior can be controlled with the ‘terminateDebuggee’ argument (if supported by the debug adapter).
8
+
9
+ register!
10
+
11
+ def execute
12
+ @session.stop!
13
+ respond!
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,27 @@
1
+ module Byebug::DAP
2
+ class Command::Evaluate < Command
3
+ # "Evaluates the given expression in the context of the top most stack frame.
4
+ # "The expression has access to any variables and arguments that are in scope.
5
+
6
+ include ValueHelpers
7
+
8
+ register!
9
+
10
+ def execute
11
+ started!
12
+
13
+ respond! body: evaluate
14
+ end
15
+
16
+ private
17
+
18
+ def evaluate
19
+ return prepare_value_response(0, 0, :evaluate) { TOPLEVEL_BINDING.eval(args.expression) } unless args.frameId
20
+
21
+ frame, thnum, frnum = resolve_frame_id(args.frameId)
22
+ return unless frame
23
+
24
+ prepare_value_response(thnum, frnum, :evaluate) { frame._binding.eval(args.expression) }
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,40 @@
1
+ module Byebug::DAP
2
+ class Command::ExceptionInfo < ContextualCommand
3
+ # "Retrieves the details of the exception that caused this event to be raised.
4
+
5
+ register!
6
+
7
+ def execute_in_context
8
+ unless ex = @processor.last_exception
9
+ respond! success: false, message: 'Not in a catchpoint context'
10
+ return
11
+ end
12
+
13
+ class_name = safe(ex, [:class, :name]) { "Unknown" }
14
+
15
+ respond! body: {
16
+ exceptionId: class_name,
17
+ description: exception_description(ex),
18
+ breakMode: ::DAP::ExceptionBreakMode::ALWAYS,
19
+ details: details(ex, '$!'),
20
+ }
21
+ end
22
+
23
+ private
24
+
25
+ def details(ex, eval_name)
26
+ class_name = safe(ex, [:class, :name]) { nil }
27
+ type_name = class_name.split('::').last if class_name
28
+ inner = safe(ex, :cause) { nil }
29
+
30
+ {
31
+ message: safe(ex, :message) { nil },
32
+ typeName: type_name,
33
+ fullTypeName: class_name,
34
+ evaluateName: eval_name,
35
+ stackTrace: safe(ex, :backtrace) { [] }.join("\n"),
36
+ innerException: inner.nil? ? [] : [details(inner, "#{eval_name}.#{cause}")],
37
+ }
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,27 @@
1
+ module Byebug::DAP
2
+ class Command::Initialize < Command
3
+ # "The ‘initialize’ request is sent as the first request from the client to the debug adapter
4
+ # "in order to configure it with client capabilities and to retrieve capabilities from the debug adapter.
5
+ # "Until the debug adapter has responded to with an ‘initialize’ response, the client must not send any additional requests or events to the debug adapter.
6
+ # "In addition the debug adapter is not allowed to send any requests or events to the client until it has responded with an ‘initialize’ response.
7
+ # "The ‘initialize’ request may only be sent once.
8
+
9
+ register!
10
+
11
+ def execute
12
+ respond! body: {
13
+ supportsConfigurationDoneRequest: true,
14
+ supportsFunctionBreakpoints: true,
15
+ supportsConditionalBreakpoints: true,
16
+ supportsHitConditionalBreakpoints: true,
17
+ supportsLogPoints: true,
18
+ supportsBreakpointLocationsRequest: true,
19
+ supportsDelayedStackTraceLoading: true,
20
+ exceptionBreakpointFilters: Command::SetExceptionBreakpoints::FILTERS,
21
+ supportsExceptionInfoRequest: true,
22
+ }
23
+
24
+ event! 'initialized'
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,13 @@
1
+ module Byebug::DAP
2
+ class Command::Launch < Command
3
+ # "This launch request is sent from the client to the debug adapter to start the debuggee with or without debugging (if ‘noDebug’ is true).
4
+
5
+ register!
6
+
7
+ def execute
8
+ stopped!
9
+ @session.start!(:launched) unless args.noDebug
10
+ respond!
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,20 @@
1
+ module Byebug::DAP
2
+ class Command::Next < 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_over(1, @context.frame.pos)
10
+ :stop
11
+ end
12
+
13
+ private
14
+
15
+ def forward_to_context(ctx)
16
+ super
17
+ respond!
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ module Byebug::DAP
2
+ class Command::Pause < ContextualCommand
3
+ # "The request suspends the debuggee.
4
+ # "The debug adapter first sends the response and then a ‘stopped’ event (with reason ‘pause’) after the thread has been paused successfully.
5
+
6
+ register!
7
+
8
+ def execute_in_context
9
+ @processor.pause_requested = true
10
+ end
11
+
12
+ private
13
+
14
+ def forward_to_context(ctx)
15
+ ctx.interrupt
16
+ super
17
+ respond!
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,56 @@
1
+ module Byebug::DAP
2
+ class Command::Scopes < Command
3
+ # "The request returns the variable scopes for a given stackframe ID.
4
+
5
+ register!
6
+
7
+ def execute
8
+ started!
9
+
10
+ frame, thnum, frnum = resolve_frame_id(args.frameId)
11
+ return unless frame
12
+
13
+ scopes = []
14
+
15
+ locals = frame_local_names(frame).sort
16
+ unless locals.empty?
17
+ scopes << ::DAP::Scope.new(
18
+ name: 'Locals',
19
+ presentationHint: 'locals',
20
+ variablesReference: @session.save_variables(thnum, frnum, :locals, locals),
21
+ namedVariables: locals.size,
22
+ indexedVariables: 0,
23
+ expensive: false)
24
+ .validate!
25
+ end
26
+
27
+ globals = global_names.sort
28
+ unless globals.empty?
29
+ scopes << ::DAP::Scope.new(
30
+ name: 'Globals',
31
+ presentationHint: 'globals',
32
+ variablesReference: @session.save_variables(thnum, frnum, :globals, globals),
33
+ namedVariables: globals.size,
34
+ indexedVariables: 0,
35
+ expensive: true)
36
+ .validate!
37
+ end
38
+
39
+ respond! body: ::DAP::ScopesResponseBody.new(scopes: scopes)
40
+ end
41
+
42
+ private
43
+
44
+ def frame_local_names(frame)
45
+ locals = frame.locals
46
+ locals = locals.keys unless locals == [] # BUG in Byebug?
47
+ locals << :self if frame._self.to_s != 'main'
48
+ locals << :$! if frame.pos == 0 && !frame.context.processor.last_exception.nil?
49
+ locals
50
+ end
51
+
52
+ def global_names
53
+ global_variables - %i[$IGNORECASE $= $KCODE $-K $binding]
54
+ end
55
+ end
56
+ end