byebug-dap 0.1.3 → 0.1.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 18d52dd5f136afc3350836a23abe3c9b957646358a566e15bfac0aedd070189c
4
- data.tar.gz: 615c83e3512cc884f1d9dd2661b578cdf4314ebe2f3bab55f4c76f70a7fb17fc
3
+ metadata.gz: 581f5d93c98be98b115c7a20ed8d4ab1579cc1bf51cd56a6bff24ff22fb76695
4
+ data.tar.gz: 4fd205fedf08c249cc2b7484a3ecffd4a7885751d030fbd99f50e167fcf9f7f4
5
5
  SHA512:
6
- metadata.gz: f5932e26ae7fe7f9518e35df4a22aad7cb363f57d56680e8416e2f6dfa3aed447b7a38270e752313b08a7244108a13f64daee74b58d715ce333b95e4230abbf8
7
- data.tar.gz: 4e150cd4ba325258f41d91496ab6d6914294d15330a124c1501ec50b28991a8619d49be3420d4f22305c5067c5d42de167a62dbf2a919b501af9dcb75c18f6f1
6
+ metadata.gz: a27c677b2a123d5a390b99a42bfe6d572b595239cf53b7ff47480c13412f8aa233ab06d40acb2b022ee53775d3d8332a2eada659f96cea2f95191216e7d333d5
7
+ data.tar.gz: 46c3a3ca0868a4a2f13c53985620504107a3897e53d5d15a7335bc61d17510766caae4f527bbac9ae24f798d9430868027ef4a43a5d526de696026632dfe9273
@@ -1,5 +1,11 @@
1
1
  # Change Log
2
2
 
3
+ ## 0.1.4
4
+
5
+ - Workaround a bug caused by
6
+ [byebug#734](https://github.com/deivid-rodriguez/byebug/issues/734) by setting
7
+ the breakpoint hit condition to `>= 0` when the condition should be `nil`.
8
+
3
9
  ## 0.1.3
4
10
 
5
11
  - Support for output capture
data/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ [![Gem Version](https://badge.fury.io/rb/byebug-dap.svg)](https://badge.fury.io/rb/byebug-dap) [![Documentation](https://img.shields.io/static/v1?label=docs&message=master&color=informational&style=flat)](https://firelizzard.gitlab.io/byebug-dap/)
2
+
1
3
  # Byebug Debug Adapter Protocol
2
4
 
3
5
  This gem adds [Debug Adapter
@@ -8,7 +8,7 @@ Usage: byebug-dap [options] <--stdio|--unix dap.socket|--listen 12345> <program>
8
8
  EOS
9
9
 
10
10
  def next_arg
11
- arg = ARGV.pop
11
+ arg = ARGV.shift
12
12
  return arg if arg
13
13
 
14
14
  LOG.puts USAGE
@@ -88,7 +88,13 @@ begin
88
88
 
89
89
  LOG.puts "ok" unless options[:start_code]
90
90
 
91
- require File.realpath(program)
91
+ if File.exist?(program)
92
+ require program
93
+ elsif defined?(Bundler)
94
+ Bundler::CLI.start(['exec', program, *ARGV])
95
+ else
96
+ require program
97
+ end
92
98
 
93
99
  rescue => e
94
100
  LOG.puts "#{e.message} (#{e.class.name})", *e.backtrace
@@ -5,6 +5,11 @@ require 'byebug/remote'
5
5
 
6
6
  require_relative 'gem'
7
7
 
8
+ module Byebug::DAP
9
+ # An alias for `ruby-dap`'s {DAP} module.
10
+ Protocol = ::DAP
11
+ end
12
+
8
13
  # load helpers
9
14
  Dir[File.join(__dir__, 'dap', 'helpers', '*.rb')].each { |file| require file }
10
15
 
@@ -22,8 +27,13 @@ require_relative 'dap/server'
22
27
 
23
28
  module Byebug
24
29
  class << self
25
- def start_dap(host, port = 0, &block)
26
- DAP::Server.new(&block).start(host, port)
30
+ # Creates and starts the server. See {DAP::Server#initialize} and
31
+ # {DAP::Server#start}.
32
+ # @param host the host passed to {DAP::Server#start}
33
+ # @param port the port passed to {DAP::Server#start}
34
+ # @return [DAP::Server]
35
+ def start_dap(host, port = 0)
36
+ DAP::Server.new.start(host, port)
27
37
  end
28
38
  end
29
39
 
@@ -37,28 +47,31 @@ module Byebug
37
47
  end
38
48
 
39
49
  module Byebug::DAP
40
- Protocol = ::DAP
41
-
42
50
  class << self
43
- def child_spawned(*args)
44
- Session.child_spawned(*args)
45
- end
46
-
51
+ # (see Session.stop!)
47
52
  def stop!
48
- interface = Byebug::Context.interface
49
- return false unless interface.is_a?(Session)
53
+ Session.stop!
54
+ end
50
55
 
51
- interface.stop!
52
- true
56
+ # (see Session.child_spawned)
57
+ def child_spawned(*args)
58
+ Session.child_spawned(*args)
53
59
  end
54
60
  end
55
61
  end
56
62
 
63
+ # Debug logging
57
64
  module Byebug::DAP::Debug
58
65
  class << self
59
66
  @protocol = false
60
67
  @evaluate = false
61
68
 
62
- attr_accessor :protocol, :evaluate
69
+ # Log all sent and received protocol messages.
70
+ # @return [Boolean]
71
+ attr_accessor :protocol
72
+
73
+ # Log evaluation failures.
74
+ # @return [Boolean]
75
+ attr_accessor :evaluate
63
76
  end
64
77
  end
@@ -1,9 +1,14 @@
1
1
  module Byebug::DAP
2
+ # Implementation of a DAP command.
3
+ # @abstract Subclasses must implement {#execute}
2
4
  class Command
5
+ # The error message returned when a variable or expression cannot be evaluated.
3
6
  EVAL_ERROR = "*Error in evaluation*"
4
7
 
5
8
  include SafeHelpers
6
9
 
10
+ # The DAP command assocated with the receiver.
11
+ # @return [std:String]
7
12
  def self.command
8
13
  return @command_name if defined?(@command_name)
9
14
 
@@ -15,10 +20,16 @@ module Byebug::DAP
15
20
  @command_name = "#{last[0].downcase}#{last[1..]}"
16
21
  end
17
22
 
23
+ # Register the receiver as a DAP command.
18
24
  def self.register!
19
25
  (@@commands ||= {})[command] = self
20
26
  end
21
27
 
28
+ # Resolve the requested command. Calls {Session#respond!} indicating a
29
+ # failed request if the command cannot be found.
30
+ # @param session [Session] the debug session
31
+ # @param request [Protocol::Request] the DAP request
32
+ # @return [std:Class] the {Command} class
22
33
  def self.resolve!(session, request)
23
34
  cls = @@commands[request.command]
24
35
  return cls if cls
@@ -26,21 +37,33 @@ module Byebug::DAP
26
37
  session.respond! request, success: false, message: 'Invalid command'
27
38
  end
28
39
 
40
+ # Resolve and execute the requested command. The command is {.resolve!
41
+ # resolved}, {#initialize initialized}, and {#safe_execute safely executed}.
42
+ # @param session [Session] the debug session
43
+ # @param request [Protocol::Request] the DAP request
44
+ # @param args [std:Array] additional arguments for {#initialize}
45
+ # @return the return value of {#safe_execute}
29
46
  def self.execute(session, request, *args)
30
47
  return unless command = resolve!(session, request)
31
48
 
32
49
  command.new(session, request, *args).safe_execute
33
50
  end
34
51
 
52
+ # Create a new instance of the receiver.
53
+ # @param session [Session] the debug session
54
+ # @param request [Protocol::Request] the DAP request
35
55
  def initialize(session, request)
36
56
  @session = session
37
57
  @request = request
38
58
  end
39
59
 
60
+ # (see Session#log)
40
61
  def log(*args)
41
62
  @session.log(*args)
42
63
  end
43
64
 
65
+ # Call {#execute} safely, handling any errors that arise.
66
+ # @return the return value of {#execute}
44
67
  def safe_execute
45
68
  execute
46
69
 
@@ -91,12 +114,18 @@ module Byebug::DAP
91
114
  return
92
115
  end
93
116
 
117
+ # Raises an error if the debugger is running
118
+ # @api private
119
+ # @!visibility public
94
120
  def stopped!
95
121
  return if !Byebug.started?
96
122
 
97
123
  respond! success: false, message: "Cannot #{@request.command} - debugger is already running"
98
124
  end
99
125
 
126
+ # Raises an error unless the debugger is running
127
+ # @api private
128
+ # @!visibility public
100
129
  def started!
101
130
  return if Byebug.started?
102
131
 
@@ -111,6 +140,13 @@ module Byebug::DAP
111
140
  safe(-> { "#{ex.message} (#{ex.class.name})" }, :call) { EVAL_ERROR }
112
141
  end
113
142
 
143
+ # Execute a code block on the specified thread, {SafeHelpers#safe safely}.
144
+ # @param thnum [std:Integer] the thread number
145
+ # @param block [std:Proc] the code block
146
+ # @yield called on error
147
+ # @yieldparam ex [std:Exception] the execution error
148
+ # @api private
149
+ # @!visibility public
114
150
  def execute_on_thread(thnum, block, &on_error)
115
151
  return safe(block, :call, &on_error) if thnum == 0 || @context&.thnum == thnum
116
152
 
@@ -207,8 +243,10 @@ module Byebug::DAP
207
243
  end
208
244
 
209
245
  def convert_breakpoint_hit_condition(condition)
210
- return nil if condition.nil? || condition.empty?
211
- return nil unless condition.is_a?(String)
246
+ # Because of https://github.com/deivid-rodriguez/byebug/issues/739,
247
+ # Breakpoint#hit_condition can't be set to nil.
248
+ return :ge, 0 if condition.nil? || condition.empty?
249
+ return :ge, 0 unless condition.is_a?(String)
212
250
 
213
251
  m = /^(?<op><|<=|=|==|===|=>|>|%)?\s*(?<value>[0-9]+)$/.match(condition)
214
252
  raise InvalidRequestArgumentError.new("'#{condition}' is not a valid hit condition") unless m
@@ -1,10 +1,14 @@
1
1
  module Byebug
2
2
  module DAP
3
+ # Processes thread-specific commands and handles Byebug/TracePoint events.
3
4
  class CommandProcessor
4
5
  extend Forwardable
5
6
  include SafeHelpers
6
7
 
8
+ # Indicates a timeout while sending a message to the context.
7
9
  class TimeoutError < StandardError
10
+ # The receiving context.
11
+ # @return [gem:byebug:Byebug::Context]
8
12
  attr_reader :context
9
13
 
10
14
  def initialize(context)
@@ -12,9 +16,25 @@ module Byebug
12
16
  end
13
17
  end
14
18
 
15
- attr_reader :context, :last_exception
19
+ # The thread context.
20
+ # @return [gem:byebug:Byebug::Context]
21
+ attr_reader :context
22
+
23
+ # The last exception that occured.
24
+ # @return [std:Exception]
25
+ attr_reader :last_exception
26
+
27
+ # Indicates that the client requested a pause.
28
+ # @return [Boolean]
29
+ # @note This should only be set by {Command::Pause}
30
+ # @api private
16
31
  attr_writer :pause_requested
17
32
 
33
+ # Create a new command processor.
34
+ # @param context [gem:byebug:Byebug::Context] the thread context
35
+ # @param session [Session] the debugging session
36
+ # @note This should only be used by Byebug internals
37
+ # @api private
18
38
  def initialize(context, session)
19
39
  @context = context
20
40
  @session = session
@@ -23,14 +43,21 @@ module Byebug
23
43
  @exec_ch = Channel.new
24
44
  end
25
45
 
46
+ # (see Session#log)
26
47
  def log(*args)
27
48
  @session.log(*args)
28
49
  end
29
50
 
51
+ # Send a message to the thread context.
52
+ # @param message the message to send
53
+ # @note Raises a {TimeoutError timeout error} after 1 second if the thread is not paused or not responding.
30
54
  def <<(message)
31
55
  @requests.push(message, timeout: 1) { raise TimeoutError.new(context) }
32
56
  end
33
57
 
58
+ # Execute a code block in the thread.
59
+ # @yield the code block to execute
60
+ # @note This calls {#\<\<} and thus may raise a {TimeoutError timeout error}.
34
61
  def execute(&block)
35
62
  raise "Block required" unless block_given?
36
63
 
@@ -47,6 +74,51 @@ module Byebug
47
74
  end
48
75
  end
49
76
 
77
+ # Line handler.
78
+ # @note This should only be called by Byebug internals
79
+ # @api private
80
+ def at_line
81
+ stopped!
82
+ end
83
+
84
+ # End of class/module handler.
85
+ # @note This should only be called by Byebug internals
86
+ # @api private
87
+ def at_end
88
+ stopped!
89
+ end
90
+
91
+ # Return handler.
92
+ # @note This should only be called by Byebug internals
93
+ # @api private
94
+ def at_return(return_value)
95
+ @at_return = return_value
96
+ stopped!
97
+ end
98
+
99
+ # Tracing handler.
100
+ # @note This should only be called by Byebug internals
101
+ # @api private
102
+ def at_tracing
103
+ # @session.puts "Tracing: #{context.full_location}"
104
+ end
105
+
106
+ # Breakpoint handler.
107
+ # @note This should only be called by Byebug internals
108
+ # @api private
109
+ def at_breakpoint(breakpoint)
110
+ @last_breakpoint = breakpoint
111
+ end
112
+
113
+ # Catchpoint handler.
114
+ # @note This should only be called by Byebug internals
115
+ # @api private
116
+ def at_catchpoint(exception)
117
+ @last_exception = exception
118
+ end
119
+
120
+ private
121
+
50
122
  def process_requests
51
123
  loop do
52
124
  request = @requests.pop
@@ -69,35 +141,6 @@ module Byebug
69
141
  log "\n! #{e.message} (#{e.class})", *e.backtrace
70
142
  end
71
143
 
72
- def logpoint!
73
- return false unless @last_breakpoint
74
-
75
- breakpoint, @last_breakpoint = @last_breakpoint, nil
76
- expr = @session.get_log_point(breakpoint)
77
- return false unless expr
78
-
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
83
-
84
- body = {
85
- category: 'console',
86
- output: msg + "\n",
87
- }
88
-
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
- end
96
-
97
- @session.event! 'output', **body
98
- return true
99
- end
100
-
101
144
  def stopped!
102
145
  return if logpoint!
103
146
 
@@ -141,30 +184,33 @@ module Byebug
141
184
  process_requests
142
185
  end
143
186
 
144
- alias at_line stopped!
145
- alias at_end stopped!
146
-
147
- def at_end
148
- stopped!
149
- end
187
+ def logpoint!
188
+ return false unless @last_breakpoint
150
189
 
151
- def at_return(return_value)
152
- @at_return = return_value
153
- stopped!
154
- end
190
+ breakpoint, @last_breakpoint = @last_breakpoint, nil
191
+ expr = @session.get_log_point(breakpoint)
192
+ return false unless expr
155
193
 
156
- # def at_tracing
157
- # @session.puts "Tracing: #{context.full_location}"
194
+ binding = @context.frame._binding
195
+ msg = expr.gsub(/\{([^\}]+)\}/) do |x|
196
+ safe(binding, :eval, x[1...-1]) { return true } # ignore bad log points
197
+ end
158
198
 
159
- # # run_auto_cmds(2)
160
- # end
199
+ body = {
200
+ category: 'console',
201
+ output: msg + "\n",
202
+ }
161
203
 
162
- def at_breakpoint(breakpoint)
163
- @last_breakpoint = breakpoint
164
- end
204
+ if breakpoint.pos.is_a?(Integer)
205
+ body[:line] = breakpoint.pos
206
+ body[:source] = {
207
+ name: File.basename(breakpoint.source),
208
+ path: breakpoint.source,
209
+ }
210
+ end
165
211
 
166
- def at_catchpoint(exception)
167
- @last_exception = exception
212
+ @session.event! 'output', **body
213
+ return true
168
214
  end
169
215
  end
170
216
  end
@@ -11,6 +11,7 @@ module Byebug::DAP
11
11
  def execute
12
12
  @session.stop!
13
13
  respond!
14
+ event! 'terminated'
14
15
  end
15
16
  end
16
17
  end
@@ -15,7 +15,7 @@ module Byebug::DAP
15
15
  respond! body: {
16
16
  exceptionId: class_name,
17
17
  description: exception_description(ex),
18
- breakMode: ::DAP::ExceptionBreakMode::ALWAYS,
18
+ breakMode: Protocol::ExceptionBreakMode::ALWAYS,
19
19
  details: details(ex, '$!'),
20
20
  }
21
21
  end
@@ -14,7 +14,7 @@ module Byebug::DAP
14
14
 
15
15
  locals = frame_local_names(frame).sort
16
16
  unless locals.empty?
17
- scopes << ::DAP::Scope.new(
17
+ scopes << Protocol::Scope.new(
18
18
  name: 'Locals',
19
19
  presentationHint: 'locals',
20
20
  variablesReference: @session.save_variables(thnum, frnum, :locals, locals),
@@ -26,7 +26,7 @@ module Byebug::DAP
26
26
 
27
27
  globals = global_names.sort
28
28
  unless globals.empty?
29
- scopes << ::DAP::Scope.new(
29
+ scopes << Protocol::Scope.new(
30
30
  name: 'Globals',
31
31
  presentationHint: 'globals',
32
32
  variablesReference: @session.save_variables(thnum, frnum, :globals, globals),
@@ -36,7 +36,7 @@ module Byebug::DAP
36
36
  .validate!
37
37
  end
38
38
 
39
- respond! body: ::DAP::ScopesResponseBody.new(scopes: scopes)
39
+ respond! body: Protocol::ScopesResponseBody.new(scopes: scopes)
40
40
  end
41
41
 
42
42
  private
@@ -42,7 +42,7 @@ module Byebug::DAP
42
42
  results << {
43
43
  id: bp.id,
44
44
  verified: true,
45
- source: ::DAP::Source.new(name: File.basename(cm[0]), path: cm[0]),
45
+ source: Protocol::Source.new(name: File.basename(cm[0]), path: cm[0]),
46
46
  line: cm[1]
47
47
  }
48
48
  end
@@ -51,7 +51,7 @@ module Byebug::DAP
51
51
  results << {
52
52
  id: bp.id,
53
53
  verified: true,
54
- source: ::DAP::Source.new(name: File.basename(im[0]), path: im[0]),
54
+ source: Protocol::Source.new(name: File.basename(im[0]), path: im[0]),
55
55
  line: im[1]
56
56
  }
57
57
  end
@@ -7,11 +7,11 @@ module Byebug::DAP
7
7
  def execute
8
8
  started!
9
9
 
10
- respond! body: ::DAP::ThreadsResponseBody.new(
10
+ respond! body: Protocol::ThreadsResponseBody.new(
11
11
  threads: Byebug
12
12
  .contexts
13
13
  .filter { |ctx| !ctx.thread.is_a?(::Byebug::DebugThread) }
14
- .map { |ctx| ::DAP::Thread.new(
14
+ .map { |ctx| Protocol::Thread.new(
15
15
  id: ctx.thnum,
16
16
  name: ctx.thread.name || "Thread ##{ctx.thnum}"
17
17
  ).validate! })
@@ -27,7 +27,7 @@ module Byebug::DAP
27
27
 
28
28
  variables = vars[first...last].map { |var, get| prepare_value_response(thnum, frnum, :variable, name: var) { get.call(var) } }
29
29
 
30
- respond! body: ::DAP::VariablesResponseBody.new(variables: variables)
30
+ respond! body: Protocol::VariablesResponseBody.new(variables: variables)
31
31
  end
32
32
  end
33
33
  end
@@ -1,5 +1,9 @@
1
1
  module Byebug::DAP
2
+ # Implementation of a DAP command that must be executed in-context.
3
+ # @abstract Subclasses must implement {#execute_in_context}
2
4
  class ContextualCommand < Command
5
+ # (see Command.resolve!)
6
+ # @note Raises an error if the resolved class is not a subclass of {ContextualCommand}
3
7
  def self.resolve!(session, request)
4
8
  return unless cls = super
5
9
  return cls if cls < ContextualCommand
@@ -7,12 +11,19 @@ module Byebug::DAP
7
11
  raise "Not a contextual command: #{command}"
8
12
  end
9
13
 
14
+ # Create a new instance of the receiver.
15
+ # @param session [Session] the debug session
16
+ # @param request [Protocol::Request] the DAP request
17
+ # @param processor [CommandProcessor] the command processor associated with the context
10
18
  def initialize(session, request, processor = nil)
11
19
  super(session, request)
12
20
  @processor = processor
13
21
  @context = processor&.context
14
22
  end
15
23
 
24
+ # {#execute_in_context Execute in-context} if `processor` is defined.
25
+ # Otherwise, ensure debugging is {#started! started}, find the requested
26
+ # thread context, and {#forward_to_context forward the request}.
16
27
  def execute
17
28
  return execute_in_context if @processor
18
29
 
@@ -23,6 +34,10 @@ module Byebug::DAP
23
34
 
24
35
  private
25
36
 
37
+ # Forward the request to the context's thread.
38
+ # @param ctx [gem:byebug:Byebug::Context] the context
39
+ # @api private
40
+ # @!visibility public
26
41
  def forward_to_context(ctx)
27
42
  ctx.processor << @request
28
43
  end
@@ -1,5 +1,12 @@
1
1
  module Byebug::DAP
2
+ # Captures STDOUT and STDERR. See {CapturedOutput}.
3
+ # @api private
2
4
  class CapturedIO
5
+ # Capture STDOUT and STDERR and create a new
6
+ # {gem:byebug:Byebug::DebugThread} running {#capture}. See
7
+ # {CapturedOutput#initialize}.
8
+ # @param forward_stdout [Boolean] if true, captured STDOUT is forwarded to the original STDOUT.
9
+ # @param forward_stderr [Boolean] if true, captured STDERR is forwarded to the original STDERR.
3
10
  def initialize(forward_stdout, forward_stderr)
4
11
  @forward_stdout = forward_stdout
5
12
  @forward_stderr = forward_stderr
@@ -10,6 +17,8 @@ module Byebug::DAP
10
17
  Byebug::DebugThread.new { capture }
11
18
  end
12
19
 
20
+ # Return an IO that can be used for logging.
21
+ # @return [std:IO]
13
22
  def log
14
23
  if defined?(LOG)
15
24
  LOG
@@ -20,6 +29,7 @@ module Byebug::DAP
20
29
  end
21
30
  end
22
31
 
32
+ # {CapturedOutput#restore Restore} the original STDOUT and STDERR.
23
33
  def restore
24
34
  @stop = true
25
35
  @stdout.restore
@@ -28,6 +38,11 @@ module Byebug::DAP
28
38
 
29
39
  private
30
40
 
41
+ # In a loop, read from the captured STDOUT and STDERR and send an output
42
+ # event to the active session's client (if there is an active session), and
43
+ # optionally forward the output to the original STDOUT/STDERR.
44
+ # @api private
45
+ # @!visibility public
31
46
  def capture
32
47
  until @stop do
33
48
  r, = IO.select([@stdout.captured, @stderr.captured])
@@ -1,7 +1,18 @@
1
1
  module Byebug::DAP
2
+ # Captures an IO output stream.
3
+ # @api private
2
4
  class CapturedOutput
3
- attr_reader :original, :captured
5
+ # The original stream, {std:IO#dup duplicated} from `io`. Writing to this IO
6
+ # will write to the original file.
7
+ # @return [std:IO]
8
+ attr_reader :original
4
9
 
10
+ # The captured stream. Captured output can be read from this IO.
11
+ # @return [std:IO]
12
+ attr_reader :captured
13
+
14
+ # Capture `io`, {std:IO#dup duplicate} the original, open an {std:IO.pipe
15
+ # pipe} pair, and {std:IO#reopen reopen} `io` to redirect it to the pipe.
5
16
  def initialize(io)
6
17
  @io = io
7
18
  @original = io.dup
@@ -11,6 +22,7 @@ module Byebug::DAP
11
22
  pw.close
12
23
  end
13
24
 
25
+ # Restore `io` to the original file.
14
26
  def restore
15
27
  @io.reopen(@original)
16
28
  @original.close
@@ -1,5 +1,7 @@
1
1
  module Byebug
2
2
  module DAP
3
+ # A channel for synchronously passing values between threads.
4
+ # @api private
3
5
  class Channel
4
6
  def initialize
5
7
  @mu = Mutex.new
@@ -8,6 +10,7 @@ module Byebug
8
10
  @have = false
9
11
  end
10
12
 
13
+ # Close the channel.
11
14
  def close
12
15
  @mu.synchronize {
13
16
  @closed = true
@@ -15,6 +18,8 @@ module Byebug
15
18
  }
16
19
  end
17
20
 
21
+ # Pop an item off the channel. Blocks until {#push} or {#close} is called.
22
+ # @return a value that was pushed or `nil` if the channel is closed.
18
23
  def pop
19
24
  synchronize_loop {
20
25
  return if @closed
@@ -29,6 +34,10 @@ module Byebug
29
34
  }
30
35
  end
31
36
 
37
+ # Push an item onto the channel. Raises an error if the channel is closed.
38
+ # If `timeout` is nil, blocks until {#push} or {#close} is called.
39
+ # @param message the value to push
40
+ # @yield called on timeout
32
41
  def push(message, timeout: nil)
33
42
  deadline = timeout + Time.now.to_f unless timeout.nil?
34
43
 
@@ -1,10 +1,24 @@
1
1
  module Byebug
2
2
  module DAP
3
- class ChildSpawnedEventBody < ::DAP::Base
4
- ::DAP::Event.bodies[:childSpawned] = self
3
+ # `childSpawned` is a custom DAP event used to notify the client that a
4
+ # child process has spawned.
5
+ # @api private
6
+ class ChildSpawnedEventBody < Protocol::Base
7
+ Protocol::Event.bodies[:childSpawned] = self
5
8
 
9
+ # The child process's name
10
+ # @return [std:String]
11
+ # @!attribute [r]
6
12
  property :name
13
+
14
+ # The child's process ID
15
+ # @return [std:Integer]
16
+ # @!attribute [r]
7
17
  property :pid
18
+
19
+ # The debug socket to connect to
20
+ # @return [std:String]
21
+ # @!attribute [r]
8
22
  property :socket
9
23
  end
10
24
  end
@@ -1,19 +1,27 @@
1
1
  module Byebug
2
2
  module DAP
3
+ # Tracks opaque handles used by DAP.
4
+ # @api private
3
5
  class Handles
4
6
  def initialize
5
7
  @mu = Mutex.new
6
8
  @entries = []
7
9
  end
8
10
 
11
+ # Delete all handles.
9
12
  def clear!
10
13
  sync { @entries = []; nil }
11
14
  end
12
15
 
16
+ # Retrieve the entry with the specified handle.
17
+ # @param id [std:Integer] the handle
18
+ # @return the entry
13
19
  def [](id)
14
20
  sync { @entries[id-1] }
15
21
  end
16
22
 
23
+ # Add a new entry.
24
+ # @return [std:Integer] the handle
17
25
  def <<(entry)
18
26
  sync do
19
27
  @entries << entry
@@ -1,7 +1,17 @@
1
1
  module Byebug
2
2
  module DAP
3
+ # Raised when the client sends a request with invalid arguments
4
+ # @api private
3
5
  class InvalidRequestArgumentError < StandardError
4
- attr_accessor :error, :value, :scope
6
+ # The error kind or message.
7
+ # @return [std:Symbol|std:String]
8
+ attr_reader :error
9
+
10
+ # The error value.
11
+ attr_reader :value
12
+
13
+ # The error scope.
14
+ attr_reader :scope
5
15
 
6
16
  def initialize(error, value: nil, scope: nil)
7
17
  @error = error
@@ -1,6 +1,14 @@
1
1
  module Byebug
2
2
  module DAP
3
+ # Methods to safely execute methods.
4
+ # @api private
3
5
  module SafeHelpers
6
+ # Safely execute `method` on `target` with `args`.
7
+ # @param target the receiver
8
+ # @param method [std:Symbol] the method name
9
+ # @param args [std:Array] the method arguments
10
+ # @yield called on error
11
+ # @yieldparam ex [std:StandardError] the execution error
4
12
  def safe(target, method, *args, &block)
5
13
  if method.is_a?(Array) && args.empty?
6
14
  method.each { |m| target = target.__send__(m) }
@@ -1,5 +1,12 @@
1
1
  module Byebug::DAP
2
+ # Used in case statements to identify scalar types.
3
+ # @api private
2
4
  module Scalar
5
+ # Match scalar values. {std:NilClass nil}, {std:TrueClass true},
6
+ # {std:FalseClass false}, {std:String strings}, {std:Numeric numbers},
7
+ # {std:Time times}, {std:Range ranges}, {std:date:Date dates}, and
8
+ # {std:date:DateTime date-times} are considered scalars.
9
+ # @return [Boolean]
3
10
  def ===(value)
4
11
  case value
5
12
  when nil, true, false
@@ -1,4 +1,6 @@
1
1
  module Byebug::DAP
2
+ # A debug session 'connection' using STDIN and STDOUT.
3
+ # @api private
2
4
  class STDIO
3
5
  extend Forwardable
4
6
 
@@ -1,5 +1,11 @@
1
1
  module Byebug::DAP
2
+ # Methods to prepare values for DAP responses.
3
+ # @api private
2
4
  module ValueHelpers
5
+ # Safely inspect a value and retrieve its class name, class and instance
6
+ # variables, and indexed members. {Scalar} values do not have variables or
7
+ # members. Only {std:Array arrays} and {std:Hash hashes} have members.
8
+ # @return `val.inspect`, `val.class.name`, variables, and members.
3
9
  def prepare_value(val)
4
10
  str = safe(val, :inspect) { safe(val, :to_s) { return yield } }
5
11
  cls = safe(val, :class) { nil }
@@ -8,8 +14,8 @@ module Byebug::DAP
8
14
  scalar = safe(-> { Scalar === val }, :call) { true }
9
15
  return str, typ, [], [] if scalar
10
16
 
11
- named = safe(val, :instance_variables) { [] }
12
- named += safe(val, :class_variables) { [] }
17
+ named = safe(val, :instance_variables) { [] } || []
18
+ named += safe(val, :class_variables) { [] } || []
13
19
  # named += safe(val, :constants) { [] }
14
20
 
15
21
  indexed = safe(-> {
@@ -21,6 +27,15 @@ module Byebug::DAP
21
27
  return str, typ, named, indexed
22
28
  end
23
29
 
30
+ # Prepare a {Protocol::Variable} or {Protocol::EvaluateResponseBody} for a
31
+ # calculated value. For global variables and evaluations, `thnum` and
32
+ # `frnum` should be 0. Local variables and evaluations are
33
+ # {Command#execute_on_thread executed on the specified thread}.
34
+ # @param thnum [std:Integer] the thread number
35
+ # @param frnum [std:Integer] the frame number
36
+ # @param kind [std:Symbol] `:variable` or `:evaluate`
37
+ # @param name [std:String] the variable name (ignored for evaluations)
38
+ # @yield retrieves an variable or evaluates an expression
24
39
  def prepare_value_response(thnum, frnum, kind, name: nil, &block)
25
40
  err = nil
26
41
  raw = execute_on_thread(thnum, block) { |e| err = e; nil }
@@ -39,10 +54,10 @@ module Byebug::DAP
39
54
 
40
55
  case kind
41
56
  when :variable
42
- klazz = ::DAP::Variable
57
+ klazz = Protocol::Variable
43
58
  args = { name: safe(name, :to_s) { safe(name, :inspect) { '???' } }, value: value, type: type }
44
59
  when :evaluate
45
- klazz = ::DAP::EvaluateResponseBody
60
+ klazz = Protocol::EvaluateResponseBody
46
61
  args = { result: value, type: type }
47
62
  end
48
63
 
@@ -1,6 +1,10 @@
1
1
  module Byebug
2
2
  module DAP
3
+ # Byebug DAP Server
3
4
  class Server
5
+ # Create a new server.
6
+ # @param capture [Boolean] if `true`, the debugee's STDOUT and STDERR will be captured
7
+ # @param forward [Boolean] if `false`, the debugee's STDOUT and STDERR will be supressed
4
8
  def initialize(capture: true, forward: true)
5
9
  @started = false
6
10
  @mu = Mutex.new
@@ -10,6 +14,12 @@ module Byebug
10
14
  @forward = forward
11
15
  end
12
16
 
17
+ # Starts the server. Calls {#start_stdio} if `host == :stdio`. Calls
18
+ # {#start_unix} with `port` if `host == :unix`. Calls {#start_tcp} with
19
+ # `host` and `port` otherwise.
20
+ # @param host `:stdio`, `:unix`, or the TCP host name
21
+ # @param port the Unix socket path or TCP port
22
+ # @return [Server]
13
23
  def start(host, port = 0)
14
24
  case host
15
25
  when :stdio
@@ -21,6 +31,10 @@ module Byebug
21
31
  end
22
32
  end
23
33
 
34
+ # Starts the server, listening on a TCP socket.
35
+ # @param host [std:String] the IP to listen on
36
+ # @param port [std:Number] the port to listen on
37
+ # @return [Server]
24
38
  def start_tcp(host, port)
25
39
  return if @started
26
40
  @started = true
@@ -29,6 +43,9 @@ module Byebug
29
43
  launch_accept TCPServer.new(host, port)
30
44
  end
31
45
 
46
+ # Starts the server, listening on a Unix socket.
47
+ # @param socket [std:String] the Unix socket path
48
+ # @return [Server]
32
49
  def start_unix(socket)
33
50
  return if @started
34
51
  @started = true
@@ -37,6 +54,8 @@ module Byebug
37
54
  launch_accept UNIXServer.new(socket)
38
55
  end
39
56
 
57
+ # Starts the server using STDIN and STDOUT to communicate.
58
+ # @return [Server]
40
59
  def start_stdio
41
60
  return if @started
42
61
  @started = true
@@ -47,6 +66,7 @@ module Byebug
47
66
  launch stream
48
67
  end
49
68
 
69
+ # Blocks until a client connects and begins debugging.
50
70
  def wait_for_client
51
71
  @mu.synchronize do
52
72
  loop do
@@ -1,24 +1,42 @@
1
1
  module Byebug
2
2
  module DAP
3
+ # A Byebug DAP session
3
4
  class Session
4
5
  include SafeHelpers
5
6
 
6
- @@children = []
7
+ # Call {Session#stop!} on {gem:byebug:Byebug::Context.interface} if it is
8
+ # a {Session}.
9
+ # @return [Boolean] whether {gem:byebug:Byebug::Context.interface} was a {Session}
10
+ def self.stop!
11
+ session = Byebug::Context.interface
12
+ return false unless session.is_a?(Session)
7
13
 
14
+ session.stop!
15
+ true
16
+ end
17
+
18
+ # Record a {ChildSpawnedEventBody childSpawned event} and send the event
19
+ # to the current session's client, if
20
+ # {gem:byebug:Byebug::Context.interface} is a {Session}.
8
21
  def self.child_spawned(name, pid, socket)
9
22
  child = ChildSpawnedEventBody.new(name: name, pid: pid, socket: socket)
10
- @@children << child
23
+ (@@children ||= []) << child
11
24
 
12
25
  session = Context.interface
13
- session.event! child if session.is_a?(Byebug::DAP::Session)
26
+ return false unless session.is_a?(Session)
14
27
 
28
+ session.event! child
15
29
  return true
16
30
 
17
31
  rescue IOError, Errno::EPIPE, Errno::ECONNRESET, Errno::ECONNABORTED
18
32
  return false
19
33
  end
20
34
 
21
- def initialize(connection, ios, &block)
35
+ # Create a new session instance.
36
+ # @param connection [std:IO] the connection to the client
37
+ # @param ios [CapturedIO] the captured IO
38
+ # @yield called once the client is done configuring the session (optional)
39
+ def initialize(connection, ios = nil, &block)
22
40
  @connection = connection
23
41
  @ios = ios
24
42
  @on_configured = block
@@ -31,6 +49,7 @@ module Byebug
31
49
  notify_of_children
32
50
  end
33
51
 
52
+ # Write a message to the log.
34
53
  def log(*args)
35
54
  logger =
36
55
  if @ios
@@ -43,20 +62,28 @@ module Byebug
43
62
  logger.puts(*args)
44
63
  end
45
64
 
65
+ # Execute requests from the client until the connection is closed.
46
66
  def execute
47
67
  Context.interface = self
48
- Context.processor = Byebug::DAP::CommandProcessor
68
+ Context.processor = DAP::CommandProcessor
49
69
 
50
70
  Command.execute(self, receive) until @connection.closed?
51
71
 
52
72
  Context.interface = LocalInterface.new
53
73
  end
54
74
 
75
+ # Invalidate frame IDs and variables references.
76
+ # @note This should only be used by a {ContextualCommand contextual command} that un-pauses its context
77
+ # @api private
55
78
  def invalidate_handles!
56
79
  @frame_ids.clear!
57
80
  @variable_refs.clear!
58
81
  end
59
82
 
83
+ # Start Byebug.
84
+ # @param mode [std:Symbol] `:attached` or `:launched`
85
+ # @note This should only be used by the {Command::Attach attach} or {Command::Launch launch} commands
86
+ # @api private
60
87
  def start!(mode)
61
88
  @trace.enable
62
89
  Byebug.mode = mode
@@ -64,6 +91,9 @@ module Byebug
64
91
  @exit_on_stop = true if mode == :launched
65
92
  end
66
93
 
94
+ # Call the block passed to {#initialize}.
95
+ # @note This should only be used by the {Command::ConfigurationDone configurationDone} commands
96
+ # @api private
67
97
  def configured!
68
98
  return unless @on_configured
69
99
 
@@ -71,6 +101,8 @@ module Byebug
71
101
  callback.call
72
102
  end
73
103
 
104
+ # Stop Byebug and close the client's connection.
105
+ # @note If the session was started with the `launch` command, this will {std:Kernel#exit exit}
74
106
  def stop!
75
107
  exit if @exit_on_stop && @pid == Process.pid
76
108
 
@@ -80,19 +112,29 @@ module Byebug
80
112
  @connection.close
81
113
  end
82
114
 
115
+ # Send an event to the client. Either call with an event name and body
116
+ # attributes, or call with an already constructed body.
117
+ # @param event [std:String|Protocol::Base] the event name or event body
118
+ # @param values [std:Hash] event body attributes
83
119
  def event!(event, **values)
84
120
  if (cls = event.class.name.split('::').last) && cls.end_with?('EventBody')
85
121
  body, event = event, cls[0].downcase + cls[1...-9]
86
122
 
87
123
  elsif event.is_a?(String) && !values.empty?
88
- body = ::DAP.const_get("#{event[0].upcase}#{event[1..]}EventBody").new(values)
124
+ body = Protocol.const_get("#{event[0].upcase}#{event[1..]}EventBody").new(values)
89
125
  end
90
126
 
91
- send ::DAP::Event.new(event: event, body: body)
127
+ send Protocol::Event.new(event: event, body: body)
92
128
  end
93
129
 
130
+ # Send a response to the client.
131
+ # @param request [Protocol::Request] the request to respond to
132
+ # @param body [std:Hash|Protocol::Base] the response body
133
+ # @param success [std:Boolean] whether the request was successful
134
+ # @param message [std:String] the response message
135
+ # @param values [std:Hash] additional response attributes
94
136
  def respond!(request, body = nil, success: true, message: 'Success', **values)
95
- send ::DAP::Response.new(
137
+ send Protocol::Response.new(
96
138
  request_seq: request.seq,
97
139
  command: request.command,
98
140
  success: success,
@@ -101,26 +143,40 @@ module Byebug
101
143
  **values)
102
144
  end
103
145
 
146
+ # Create a variables reference.
104
147
  def save_variables(*args)
105
148
  @variable_refs << args
106
149
  end
107
150
 
151
+ # Retrieve variables from a reference.
108
152
  def restore_variables(ref)
109
153
  @variable_refs[ref]
110
154
  end
111
155
 
156
+ # Create a frame ID.
112
157
  def save_frame(*args)
113
158
  @frame_ids << args
114
159
  end
115
160
 
161
+ # Restore a frame from an ID.
116
162
  def restore_frame(id)
117
163
  @frame_ids[id]
118
164
  end
119
165
 
166
+ # Get the log point expression associated with `breakpoint`.
167
+ # @param breakpoint [gem:byebug:Byebug::Breakpoint] the breakpoint
168
+ # @return [std:String] the log point expression
169
+ # @note This should only be used by a {CommandProcessor command processor}
170
+ # @api private
120
171
  def get_log_point(breakpoint)
121
172
  @log_points[breakpoint.id]
122
173
  end
123
174
 
175
+ # Associate a log point expression with `breakpoint`.
176
+ # @param breakpoint [gem:byebug:Byebug::Breakpoint] the breakpoint
177
+ # @param expr [std:String] the log point expression
178
+ # @note This should only be used by a {CommandProcessor command processor}
179
+ # @api private
124
180
  def set_log_point(breakpoint, expr)
125
181
  if expr.nil? || expr.empty?
126
182
  @log_points.delete(breakpoint.id)
@@ -129,6 +185,9 @@ module Byebug
129
185
  end
130
186
  end
131
187
 
188
+ # Delete the specified breakpoints and any log points associated with
189
+ # them.
190
+ # @param breakpoints [std:Array<gem:byebug:Byebug::Breakpoint>] the breakpoints
132
191
  def clear_breakpoints(*breakpoints)
133
192
  breakpoints.each do |breakpoint|
134
193
  Byebug.breakpoints.delete(breakpoint)
@@ -139,7 +198,7 @@ module Byebug
139
198
  private
140
199
 
141
200
  def notify_of_children
142
- @@children.each { |c| event! c }
201
+ @@children.each { |c| event! c } if defined?(@@children)
143
202
  rescue IOError, Errno::EPIPE, Errno::ECONNRESET, Errno::ECONNABORTED
144
203
  # client is closed
145
204
  end
@@ -147,11 +206,11 @@ module Byebug
147
206
  def send(message)
148
207
  log "#{Process.pid} > #{message.to_wire}" if Debug.protocol
149
208
  message.validate!
150
- @connection.write ::DAP::Encoding.encode(message)
209
+ @connection.write Protocol.encode(message)
151
210
  end
152
211
 
153
212
  def receive
154
- m = ::DAP::Encoding.decode(@connection)
213
+ m = Protocol.decode(@connection)
155
214
  log "#{Process.pid} < #{m.to_wire}" if Debug.protocol
156
215
  m
157
216
  end
@@ -1,11 +1,25 @@
1
1
  module Byebug
2
+ # Debug Adapter Protocol support for Byebug
2
3
  module DAP
4
+ # Gem name
3
5
  NAME = 'byebug-dap'
4
- VERSION = '0.1.3'
6
+
7
+ # Gem version
8
+ VERSION = '0.1.4'
9
+
10
+ # Gem summary
5
11
  SUMMARY = 'Debug Adapter Protocol for Byebug'
12
+
13
+ # Gem description
6
14
  DESCRIPTION = 'Implements a Debug Adapter Protocol interface for Byebug'
15
+
16
+ # Gem authors
7
17
  AUTHORS = ['Ethan Reesor']
18
+
19
+ # Gem website
8
20
  WEBSITE = 'https://gitlab.com/firelizzard/byebug-dap'
21
+
22
+ # Gem license
9
23
  LICENSE = 'Apache-2.0'
10
24
  end
11
25
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: byebug-dap
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ethan Reesor
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-10-12 00:00:00.000000000 Z
11
+ date: 2020-10-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: byebug