byebug-dap 0.1.0

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3396f670f640d236adac1e58a6dd95af568925af57e9215262a0c10cd870d8df
4
+ data.tar.gz: 516cc5caca246d9a2764c1cc7ec72b5e1ed2ce27bfcca04a9c7ce1413ce720c3
5
+ SHA512:
6
+ metadata.gz: cc4611e07bc8c7738335c1a27340849ddb59a0728ce880926e99753d5168cd1f03f8e6197ba375fe074d852eaa502fb824e2555f39c76195ca301e9b4100926b
7
+ data.tar.gz: c6af798756bb6d6ab71089f365b5cc869dd1c0fde932960f1b450c198439c4bb7be0ff385a22ebc9bcc486ff5465ee937bba9836ef367857c88dd9d8f0660d15
data/AUTHORS ADDED
@@ -0,0 +1,7 @@
1
+ # This is the list of ruby-dap authors for copyright purposes.
2
+ #
3
+ # This does not necessarily list everyone who has contributed code, since in
4
+ # some cases, their employer may be the copyright holder. To see the full list
5
+ # of contributors, see the revision history in source control.
6
+
7
+ Ethan Reesor
data/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require 'byebug/dap'
5
+
6
+ USAGE = <<-EOS
7
+ Usage: byebug-dap [options] <--stdio|--unix dap.socket|--listen 12345> <program>
8
+ EOS
9
+
10
+ def next_arg
11
+ arg = ARGV.pop
12
+ return arg if arg
13
+
14
+ STDERR.puts USAGE
15
+ exit!
16
+ end
17
+
18
+ options = {}
19
+ OptionParser.new do |opts|
20
+ opts.banner = USAGE
21
+
22
+ opts.on("--stdio", "Listen on STDIN and STDOUT") { |v| options[:stdio] = v }
23
+ opts.on("--listen PORT", "Listen on a TCP port") { |v| options[:listen] = v }
24
+ opts.on("--unix SOCKET", "Listen on a unix socket") { |v| options[:unix] = v }
25
+ opts.on("-w", "--[no-]wait", "Wait for attach or launch command before running program") { |v| options[:wait] = v }
26
+ opts.on("-f", "--[no-]force", "When listening on a unix socket, delete the socket if it exists") { |v| options[:force] = v }
27
+ opts.on("--debug-protocol", "Debug DAP") { |v| Byebug::DAP::Debug.protocol = true if v }
28
+ opts.on("--debug-evaluate", "Debug variable evaluation") { |v| Byebug::DAP::Debug.evaluate = true if v }
29
+ end.parse!
30
+
31
+ program = next_arg
32
+ if program == '-'
33
+ program = next_arg
34
+ options[:stdio] = true
35
+ end
36
+
37
+ if options[:stdio]
38
+ host, port = :stdio, nil
39
+
40
+ elsif options[:listen]
41
+ host, port = options[:listen].split(':')
42
+ host, port = 'localhost', host unless port
43
+
44
+ elsif options[:unix]
45
+ host, port = :unix, options[:unix]
46
+
47
+ if File.exist?(port)
48
+ if options[:force]
49
+ File.delete(port)
50
+ else
51
+ puts "#{port} already exists"
52
+ exit!
53
+ end
54
+ end
55
+
56
+ else
57
+ STDERR.puts USAGE, "One of --stdio, --listen, or --unix is required"
58
+ exit!
59
+ end
60
+
61
+ begin
62
+ STDERR.puts "Starting DAP"
63
+ STDERR.flush
64
+
65
+ if options[:wait]
66
+ Byebug.start_dap(host, port) { require File.realpath(program) }
67
+ else
68
+ Byebug.start_dap(host, port)
69
+ require File.realpath(program)
70
+ end
71
+
72
+ ensure
73
+ File.delete(port) if File.exist?(port) if host == :unix
74
+ end
@@ -0,0 +1,33 @@
1
+ require 'dap'
2
+ require 'byebug'
3
+ require 'byebug/core'
4
+ require 'byebug/remote'
5
+
6
+ require_relative 'dap/channel'
7
+ require_relative 'dap/handles'
8
+ require_relative 'dap/invalid_request_argument_error'
9
+ require_relative 'dap/safe_helpers'
10
+
11
+ require_relative 'dap/server'
12
+ require_relative 'dap/command_processor'
13
+ require_relative 'dap/controller'
14
+ require_relative 'dap/interface'
15
+
16
+ module Byebug
17
+ module DAP
18
+ module Debug
19
+ class << self
20
+ @protocol = false
21
+ @evaluate = false
22
+
23
+ attr_accessor :protocol, :evaluate
24
+ end
25
+ end
26
+ end
27
+
28
+ class << self
29
+ def start_dap(host, port = 0, &block)
30
+ DAP::Server.new(&block).start(host, port)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,64 @@
1
+ module Byebug
2
+ module DAP
3
+ class Channel
4
+ def initialize
5
+ @mu = Mutex.new
6
+ @cond = ConditionVariable.new
7
+ @closed = false
8
+ @have = false
9
+ end
10
+
11
+ def close
12
+ @mu.synchronize {
13
+ @closed = true
14
+ @cond.broadcast
15
+ }
16
+ end
17
+
18
+ def pop
19
+ synchronize_loop {
20
+ return if @closed
21
+
22
+ if @have
23
+ @cond.signal
24
+ @have = false
25
+ return @value
26
+ end
27
+
28
+ @cond.wait(@mu)
29
+ }
30
+ end
31
+
32
+ def push(message, timeout: nil)
33
+ deadline = timeout + Time.now.to_f unless timeout.nil?
34
+
35
+ synchronize_loop {
36
+ raise RuntimeError, "Send on closed channel" if @closed
37
+
38
+ unless @have
39
+ @cond.signal
40
+ @have = true
41
+ @value = message
42
+ return
43
+ end
44
+
45
+ if timeout.nil?
46
+ @cond.wait(@mu)
47
+
48
+ else
49
+ remaining = deadline - Time.now.to_f
50
+ return yield if remaining < 0
51
+
52
+ @cond.wait(@mu, remaining)
53
+ end
54
+ }
55
+ end
56
+
57
+ private
58
+
59
+ def synchronize_loop
60
+ @mu.synchronize { loop { yield } }
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,172 @@
1
+ module Byebug
2
+ module DAP
3
+ class CommandProcessor
4
+ extend Forwardable
5
+ include SafeHelpers
6
+
7
+ class TimeoutError < StandardError
8
+ attr_reader :context
9
+
10
+ def initialize(context)
11
+ @context = context
12
+ end
13
+ end
14
+
15
+ attr_reader :context, :interface
16
+
17
+ def initialize(context, interface)
18
+ @context = context
19
+ @interface = interface
20
+ @requests = Channel.new
21
+ @exec_mu = Mutex.new
22
+ @exec_ch = Channel.new
23
+ end
24
+
25
+ def <<(message)
26
+ @requests.push(message, timeout: 1) { raise TimeoutError.new(context) }
27
+ end
28
+
29
+ def execute(&block)
30
+ raise "Block required" unless block_given?
31
+
32
+ r, err = nil, nil
33
+ @exec_mu.synchronize {
34
+ self << block
35
+ r, err = @exec_ch.pop
36
+ }
37
+
38
+ if err
39
+ raise err
40
+ else
41
+ r
42
+ end
43
+ end
44
+
45
+ def process_requests
46
+ loop do
47
+ request = @requests.pop
48
+ break unless request
49
+
50
+ if request.is_a?(Proc)
51
+ err = nil
52
+ r = safe(request, :call) { |e| err = e; nil }
53
+ @exec_ch.push [r, err]
54
+ next
55
+ end
56
+
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.
66
+
67
+ @pause_requested = true
68
+
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.
72
+
73
+ context.step_over(1, context.frame.pos)
74
+ break
75
+
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.
83
+
84
+ context.step_into(1, context.frame.pos)
85
+ break
86
+
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.
90
+
91
+ context.step_out(context.frame.pos + 1, false)
92
+ context.frame = 0
93
+ break
94
+ end
95
+ end
96
+
97
+ interface.invalidate_handles!
98
+
99
+ rescue StandardError => e
100
+ STDERR.puts "\n! #{e.message} (#{e.class})", *e.backtrace
101
+ end
102
+
103
+ def stopped!
104
+ case context.stop_reason
105
+ when :breakpoint
106
+ number = Byebug.breakpoints.index(@at_breakpoint) + 1
107
+
108
+ args = {
109
+ reason: 'breakpoint',
110
+ description: 'Hit breakpoint',
111
+ text: "Stopped by breakpoint #{number} at #{context.frame.file}:#{context.frame.line}",
112
+ }
113
+
114
+ when :catchpoint
115
+ # TODO this is probably not the right message
116
+ args = {
117
+ reason: 'catchpoint',
118
+ description: 'Hit catchpoint',
119
+ text: "Stopped by catchpoint at #{context.location}: `#{@at_catchpoint}'",
120
+ }
121
+
122
+ when :step
123
+ if @pause_requested
124
+ @pause_requested = false
125
+ args = {
126
+ reason: 'pause',
127
+ text: "Paused at #{context.frame.file}:#{context.frame.line}"
128
+ }
129
+ else
130
+ args = {
131
+ reason: 'step',
132
+ text: "Stepped at #{context.frame.file}:#{context.frame.line}"
133
+ }
134
+ end
135
+
136
+ else
137
+ STDERR.puts "Stopped for unknown reason: #{context.stop_reason}"
138
+ end
139
+
140
+ interface.event! 'stopped', threadId: context.thnum, **args if args
141
+
142
+ process_requests
143
+ end
144
+
145
+ alias at_line stopped!
146
+ alias at_end stopped!
147
+
148
+ def at_end
149
+ stopped!
150
+ end
151
+
152
+ def at_return(return_value)
153
+ @at_return = return_value
154
+ stopped!
155
+ end
156
+
157
+ # def at_tracing
158
+ # interface.puts "Tracing: #{context.full_location}"
159
+
160
+ # # run_auto_cmds(2)
161
+ # end
162
+
163
+ def at_breakpoint(breakpoint)
164
+ @at_breakpoint = breakpoint
165
+ end
166
+
167
+ def at_catchpoint(exception)
168
+ @at_catchpoint = exception
169
+ end
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,241 @@
1
+ module Byebug
2
+ module DAP
3
+ class Controller
4
+ class DisconnectError < StandardError; end
5
+
6
+ def initialize(interface, signal_start = nil)
7
+ @interface = interface
8
+ @signal_start = signal_start
9
+
10
+ @trace = TracePoint.new(:thread_begin, :thread_end) { |t| process_trace t }
11
+ end
12
+
13
+ def run
14
+ @trace.enable
15
+
16
+ loop do
17
+ @request = @interface.receive
18
+ process_command @request
19
+
20
+ rescue InvalidRequestArgumentError => e
21
+ handle_error e
22
+
23
+ rescue CommandProcessor::TimeoutError => e
24
+ respond! success: false, message: "Debugger on thread ##{e.context.thnum} is not responding"
25
+ end
26
+
27
+ rescue IOError, Errno::EPIPE, Errno::ECONNRESET, Errno::ECONNABORTED, DisconnectError
28
+ STDERR.puts "\nClient disconnected"
29
+
30
+ rescue StandardError => e
31
+ STDERR.puts "\n! #{e.message} (#{e.class})", *e.backtrace
32
+
33
+ ensure
34
+ Byebug.mode = :off
35
+ Byebug.stop
36
+ @interface.socket.close
37
+ end
38
+
39
+ private
40
+
41
+ def process_trace(trace)
42
+ ctx = Byebug.contexts.find { |c| c.thread == Thread.current }
43
+
44
+ case trace.event
45
+ when :thread_begin
46
+ @interface.event! 'thread', reason: 'started', threadId: ctx.thnum
47
+ when :thread_end
48
+ @interface.event! 'thread', reason: 'exited', threadId: ctx.thnum
49
+ end
50
+ end
51
+
52
+ def running!
53
+ raise InvalidRequestArgumentError.new(:not_running, nil) unless Byebug.started?
54
+ end
55
+
56
+ def process_command(request)
57
+ case request.command
58
+ when 'initialize'
59
+ # "The ‘initialize’ request is sent as the first request from the client to the debug adapter
60
+ # "in order to configure it with client capabilities and to retrieve capabilities from the debug adapter.
61
+ # "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.
62
+ # "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.
63
+ # "The ‘initialize’ request may only be sent once.
64
+
65
+ respond! body: ::DAP::Capabilities.new(
66
+ supportsConfigurationDoneRequest: true)
67
+
68
+ @interface.event! 'initialized'
69
+ return
70
+
71
+ when 'disconnect'
72
+ # "The ‘disconnect’ request is sent from the client to the debug adapter in order to stop debugging.
73
+ # "It asks the debug adapter to disconnect from the debuggee and to terminate the debug adapter.
74
+ # "If the debuggee has been started with the ‘launch’ request, the ‘disconnect’ request terminates the debuggee.
75
+ # "If the ‘attach’ request was used to connect to the debuggee, ‘disconnect’ does not terminate the debuggee.
76
+ # "This behavior can be controlled with the ‘terminateDebuggee’ argument (if supported by the debug adapter).
77
+
78
+ respond!
79
+ raise DisconnectError
80
+
81
+ when 'attach'
82
+ # "The attach request is sent from the client to the debug adapter to attach to a debuggee that is already running.
83
+
84
+ Byebug.mode = :attached
85
+ Byebug.start
86
+
87
+ @signal_start.call(:attach) if @signal_start
88
+
89
+ respond!
90
+ return
91
+
92
+ when 'launch'
93
+ # "This launch request is sent from the client to the debug adapter to start the debuggee with or without debugging (if ‘noDebug’ is true).
94
+
95
+ unless request.arguments.noDebug
96
+ Byebug.mode = :launched
97
+ Byebug.start
98
+ end
99
+
100
+ @signal_start.call(:launch) if @signal_start
101
+
102
+ respond!
103
+ return
104
+
105
+ when 'configurationDone'
106
+ # "This optional request indicates that the client has finished initialization of the debug adapter.
107
+
108
+ respond!
109
+ return
110
+ end
111
+
112
+ running!
113
+
114
+ case request.command
115
+ when 'pause', 'next', 'stepIn', 'stepOut', 'continue'
116
+ ctx = @interface.find_thread(request.arguments.threadId)
117
+ ctx.interrupt if request.command == 'pause'
118
+
119
+ ctx.__send__(:processor) << request
120
+ respond!
121
+
122
+ when 'evaluate'
123
+ # "Evaluates the given expression in the context of the top most stack frame.
124
+ # "The expression has access to any variables and arguments that are in scope.
125
+
126
+ respond! body: @interface.evaluate(request.arguments.frameId, request.arguments.expression)
127
+
128
+ when 'variables'
129
+ # "Retrieves all child variables for the given variable reference.
130
+ # "An optional filter can be used to limit the fetched children to either named or indexed children
131
+
132
+ variables = @interface.variables(
133
+ request.arguments.variablesReference,
134
+ at: request.arguments.start,
135
+ count: request.arguments.count,
136
+ filter: request.arguments.filter)
137
+
138
+ respond! body: ::DAP::VariablesResponseBody.new(variables: variables)
139
+
140
+ when 'scopes'
141
+ # "The request returns the variable scopes for a given stackframe ID.
142
+
143
+ respond! body: ::DAP::ScopesResponseBody.new(
144
+ scopes: @interface.scopes(request.arguments.frameId))
145
+
146
+ when 'threads'
147
+ # "The request retrieves a list of all threads.
148
+
149
+ respond! body: ::DAP::ThreadsResponseBody.new(threads: @interface.threads)
150
+
151
+ when 'stackTrace'
152
+ # "The request returns a stacktrace from the current execution state.
153
+
154
+ frames, stack_size = @interface.frames(
155
+ request.arguments.threadId,
156
+ at: request.arguments.startFrame,
157
+ count: request.arguments.levels)
158
+
159
+ respond! body: ::DAP::StackTraceResponseBody.new(
160
+ stackFrames: frames,
161
+ totalFrames: stack_size)
162
+
163
+ when 'source'
164
+ # "The request retrieves the source code for a given source reference.
165
+
166
+ path = request.arguments.source.path
167
+ if File.readable?(path)
168
+ respond! body: ::DAP::SourceResponseBody.new(content: IO.read(path))
169
+
170
+ elsif File.exist?(path)
171
+ respond! success: false, message: "Source file '#{path}' exists but cannot be read"
172
+
173
+ else
174
+ respond! success: false, message: "No source file available for '#{path}'"
175
+ end
176
+
177
+ when 'setBreakpoints'
178
+ # "Sets multiple breakpoints for a single source and clears all previous breakpoints in that source.
179
+ # "To clear all breakpoint for a source, specify an empty array.
180
+ # "When a breakpoint is hit, a ‘stopped’ event (with reason ‘breakpoint’) is generated.
181
+
182
+ path = File.realpath(request.arguments.source.path)
183
+ ::Byebug.breakpoints.each { |bp| ::Byebug::Breakpoint.remove(bp.id) if bp.source == path }
184
+
185
+ lines = ::Byebug::Breakpoint.potential_lines(path)
186
+ verified = []
187
+ request.arguments.breakpoints.each do |requested|
188
+ next unless lines.include? requested.line
189
+
190
+ bp = ::Byebug::Breakpoint.add(path, requested.line)
191
+ verified << ::DAP::Breakpoint.new(
192
+ id: bp.id,
193
+ verified: true,
194
+ line: requested.line)
195
+ end
196
+
197
+ respond! body: ::DAP::SetBreakpointsResponseBody.new(breakpoints: verified)
198
+
199
+ else
200
+ respond! success: false, message: 'Invalid command'
201
+ end
202
+ end
203
+
204
+ def respond!(body = {}, success: true, message: 'Success', **values)
205
+ # TODO make body default to nil?
206
+ @interface << ::DAP::Response.new(
207
+ request_seq: @request.seq,
208
+ command: @request.command,
209
+ success: success,
210
+ message: message,
211
+ body: body,
212
+ **values)
213
+ end
214
+
215
+ def handle_error(ex)
216
+ case ex.error
217
+ when :not_running
218
+ respond! success: false, message: "Debugger is not running"
219
+
220
+ when :missing_argument
221
+ respond! success: false, message: "Missing #{ex.scope}"
222
+
223
+ when :missing_entry
224
+ respond! success: false, message: "Invalid #{ex.scope} #{ex.value}"
225
+
226
+ when :missing_thread
227
+ respond! success: false, message: "Cannot locate thread ##{ex.value}"
228
+
229
+ when :missing_frame
230
+ respond! success: false, message: "Cannot locate frame ##{ex.value}"
231
+
232
+ when :invalid_entry
233
+ respond! success: false, message: "Error resolving #{ex.scope}: #{ex.value}"
234
+
235
+ else
236
+ raise "Unknown internal error: #{err}"
237
+ end
238
+ end
239
+ end
240
+ end
241
+ end
@@ -0,0 +1,31 @@
1
+ module Byebug
2
+ module DAP
3
+ class Handles
4
+ def initialize
5
+ @mu = Mutex.new
6
+ @entries = []
7
+ end
8
+
9
+ def clear!
10
+ sync { @entries = []; nil }
11
+ end
12
+
13
+ def [](id)
14
+ sync { @entries[id-1] }
15
+ end
16
+
17
+ def <<(entry)
18
+ sync do
19
+ @entries << entry
20
+ @entries.size
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def sync
27
+ @mu.synchronize { yield }
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,265 @@
1
+ module Byebug
2
+ module DAP
3
+ class Interface
4
+ include SafeHelpers
5
+
6
+ attr_reader :socket
7
+
8
+ def initialize(socket)
9
+ @socket = socket
10
+ end
11
+
12
+ def <<(message)
13
+ STDERR.puts "> #{message.to_wire}" if Debug.protocol
14
+ message.validate!
15
+ socket.write ::DAP::Encoding.encode(message)
16
+ end
17
+
18
+ def event!(event, **values)
19
+ body = ::DAP.const_get("#{event[0].upcase}#{event[1..]}EventBody").new(values) unless values.empty?
20
+ self << ::DAP::Event.new(event: event, body: body)
21
+ end
22
+
23
+ def receive
24
+ m = ::DAP::Encoding.decode(socket)
25
+ STDERR.puts "< #{m.to_wire}" if Debug.protocol
26
+ m
27
+ end
28
+
29
+ def invalidate_handles!
30
+ frame_ids.clear!
31
+ variable_refs.clear!
32
+ end
33
+
34
+ def threads
35
+ Byebug.contexts
36
+ .filter { |ctx| !ctx.thread.is_a?(DebugThread) }
37
+ .map { |ctx| ::DAP::Thread.new(
38
+ id: ctx.thnum,
39
+ name: ctx.thread.name || "Thread ##{ctx.thnum}")
40
+ .validate! }
41
+ end
42
+
43
+ def find_thread(id)
44
+ raise InvalidRequestArgumentError.new(:missing_argument, scope: 'thread ID') unless id
45
+
46
+ ctx = Byebug.contexts.find { |c| c.thnum == id }
47
+ raise InvalidRequestArgumentError.new(:missing_thread, value: id) unless ctx
48
+
49
+ ctx
50
+ end
51
+
52
+ def resolve_frame_id(id)
53
+ entry = frame_ids[id]
54
+ raise InvalidRequestArgumentError.new(:missing_entry, value: id, scope: 'frame ID') unless entry
55
+
56
+ thnum, frnum = entry
57
+ ctx = Byebug.contexts.find { |c| c.thnum == thnum }
58
+ raise InvalidRequestArgumentError.new(:missing_thread, value: thnum) unless ctx
59
+ raise InvalidRequestArgumentError.new(:missing_frame, value: frnum) unless frnum < ctx.stack_size
60
+
61
+ return ::Byebug::Frame.new(ctx, frnum), thnum, frnum
62
+ end
63
+
64
+ def frames(thnum, at:, count:)
65
+ ctx = find_thread(thnum)
66
+
67
+ first = at || 0
68
+ if !count
69
+ last = ctx.stack_size
70
+ else
71
+ last = first + count
72
+ if last > ctx.stack_size
73
+ last = ctx.stack_size
74
+ end
75
+ end
76
+
77
+ frames = (first...last).map do |i|
78
+ frame = ::Byebug::Frame.new(ctx, i)
79
+ ::DAP::StackFrame.new(
80
+ id: frame_ids << [ctx.thnum, i],
81
+ name: frame.deco_call,
82
+ source: ::DAP::Source.new(
83
+ name: File.basename(frame.file),
84
+ path: File.expand_path(frame.file)),
85
+ line: frame.line,
86
+ column: 0) # TODO real column
87
+ .validate!
88
+ end
89
+
90
+ return frames, ctx.stack_size
91
+ end
92
+
93
+ def resolve_variable_reference(varRef)
94
+ raise InvalidRequestArgumentError.new(:missing_argument, scope: 'variables reference') unless varRef
95
+
96
+ entry = variable_refs[varRef]
97
+ raise InvalidRequestArgumentError.new(:missing_entry, value: ref, scope: 'variables reference') unless entry
98
+
99
+ entry
100
+ end
101
+
102
+ def scopes(frameId)
103
+ raise InvalidRequestArgumentError.new(:missing_argument, scope: 'frame ID') unless frameId
104
+
105
+ frame, thnum, frnum = resolve_frame_id(frameId)
106
+ return unless frame
107
+
108
+ scopes = []
109
+
110
+ locals = frame_local_names(frame).sort
111
+ unless locals.empty?
112
+ scopes << ::DAP::Scope.new(
113
+ name: 'Locals',
114
+ presentationHint: 'locals',
115
+ variablesReference: variable_refs << [thnum, frnum, :locals, locals],
116
+ namedVariables: locals.size,
117
+ indexedVariables: 0,
118
+ expensive: false)
119
+ .validate!
120
+ end
121
+
122
+ globals = global_names.sort
123
+ unless globals.empty?
124
+ scopes << ::DAP::Scope.new(
125
+ name: 'Globals',
126
+ presentationHint: 'globals',
127
+ variablesReference: variable_refs << [thnum, frnum, :globals, globals],
128
+ namedVariables: globals.size,
129
+ indexedVariables: 0,
130
+ expensive: true)
131
+ .validate!
132
+ end
133
+
134
+ scopes
135
+ end
136
+
137
+ def variables(varRef, at:, count:, filter: nil)
138
+ thnum, frnum, kind, *entry = resolve_variable_reference(varRef)
139
+
140
+ case kind
141
+ when :locals, :globals
142
+ ctx = find_thread(thnum)
143
+ raise InvalidRequestArgumentError.new(:missing_frame, value: frnum) unless frnum < ctx.stack_size
144
+
145
+ frame = ::Byebug::Frame.new(ctx, frnum)
146
+ end
147
+
148
+ case kind
149
+ when :locals
150
+ named, indexed = entry[0], []
151
+ get = ->(key) {
152
+ return frame._self if key == :self
153
+ values ||= frame.locals
154
+ values[key]
155
+ }
156
+
157
+ when :globals
158
+ named, indexed = entry[0], []
159
+ get = ->(key) { frame._binding.eval(key.to_s) }
160
+
161
+ when :variable, :evalate
162
+ value, named, indexed = entry
163
+ get = ->(key) { value.instance_eval { binding }.eval(key.to_s) }
164
+ index = ->(key) { value[key] }
165
+
166
+ else
167
+ raise InvalidRequestArgumentError.new(:invalid_entry, value: kind, scope: 'variable scope')
168
+ end
169
+
170
+ case filter
171
+ when 'named'
172
+ indexed = []
173
+ when 'indexed'
174
+ named = []
175
+ end
176
+
177
+ vars = named.map { |k| [k, get] } + indexed.map { |k| [k, index] }
178
+
179
+ first = at || 0
180
+ last = count ? first + count : vars.size
181
+ last = vars.size unless last < vars.size
182
+
183
+ vars[first...last].map { |var, get| prepare_value_response(thnum, frnum, :variable, name: var) { get.call(var) } }
184
+ end
185
+
186
+ def evaluate(frameId, expression)
187
+ return prepare_value_response(0, 0, :evaluate) { TOPLEVEL_BINDING.eval(expression) } unless frameId
188
+
189
+ frame, thnum, frnum = resolve_frame_id(frameId)
190
+ return unless frame
191
+
192
+ prepare_value_response(thnum, frnum, :evaluate) { frame._binding.eval(expression) }
193
+ end
194
+
195
+ private
196
+
197
+ def describe_thread(context)
198
+ if context.thread.name
199
+ "##{context.thnum} (#{context.thread.name})"
200
+ else
201
+ "##{context.thnum}"
202
+ end
203
+ end
204
+
205
+ def prepare_value_response(thnum, frnum, kind, name: nil, &block)
206
+ err = nil
207
+ if thnum == 0
208
+ raw = safe(block, :call) { |e| err = e; nil }
209
+ else
210
+ processor = find_thread(thnum).__send__(:processor)
211
+ raw = safe(-> { processor.execute(&block) }, :call) { |e| err = e; nil }
212
+ end
213
+
214
+ if err.nil?
215
+ value, type, named, indexed = prepare_value(raw) { next "*Error in evaluation*", nil, [], [] }
216
+ else
217
+ type, named, indexed = nil, [], []
218
+ if err.is_a?(CommandProcessor::TimeoutError)
219
+ value = "*Thread #{describe_thread err.context} unresponsive*"
220
+ else
221
+ value = "*Error in evaluation*"
222
+ end
223
+ end
224
+
225
+ case kind
226
+ when :variable
227
+ klazz = ::DAP::Variable
228
+ args = { name: safe(name, :to_s) { safe(name, :inspect) { '???' } }, value: value, type: type }
229
+ when :evaluate
230
+ klazz = ::DAP::EvaluateResponseBody
231
+ args = { result: value, type: type }
232
+ end
233
+
234
+ if named.empty? && indexed.empty?
235
+ args[:variablesReference] = 0
236
+ else
237
+ args[:variablesReference] = variable_refs << [thnum, frnum, kind, raw, named, indexed]
238
+ args[:namedVariables] = named.size
239
+ args[:indexedVariables] = indexed.size
240
+ end
241
+
242
+ klazz.new(args).validate!
243
+ end
244
+
245
+ def frame_ids
246
+ @frame_ids ||= Handles.new
247
+ end
248
+
249
+ def variable_refs
250
+ @variable_refs ||= Handles.new
251
+ end
252
+
253
+ def frame_local_names(frame)
254
+ locals = frame.locals
255
+ locals = locals.keys unless locals == [] # BUG in Byebug?
256
+ locals << :self if frame._self.to_s != 'main'
257
+ locals
258
+ end
259
+
260
+ def global_names
261
+ global_variables - %i[$IGNORECASE $= $KCODE $-K $binding]
262
+ end
263
+ end
264
+ end
265
+ end
@@ -0,0 +1,13 @@
1
+ module Byebug
2
+ module DAP
3
+ class InvalidRequestArgumentError < StandardError
4
+ attr_accessor :error, :value, :scope
5
+
6
+ def initialize(error, value: nil, scope: nil)
7
+ @error = error
8
+ @value = value
9
+ @scope = scope
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,55 @@
1
+ module Byebug
2
+ module DAP
3
+ module SafeHelpers
4
+ module Scalar
5
+ def ===(value)
6
+ case value
7
+ when nil, true, false
8
+ return true
9
+ when ::String, ::Symbol, ::Numeric
10
+ return true
11
+ when ::Time, ::Range
12
+ true
13
+ end
14
+
15
+ return true if defined?(::Date) && ::Date === value
16
+ return true if defined?(::DateTime) && ::DateTime === value
17
+
18
+ false
19
+ end
20
+ end
21
+
22
+ def safe(target, method, *args, &block)
23
+ if target.respond_to?(method)
24
+ target.__send__(method, *args)
25
+ else
26
+ yield
27
+ end
28
+ rescue StandardError => e
29
+ STDERR.puts "\n! #{e.message} (#{e.class})", *e.backtrace if Debug.evaluate
30
+ block.parameters.empty? ? yield : yield(e)
31
+ end
32
+
33
+ def prepare_value(val)
34
+ str = safe(val, :inspect) { safe(val, :to_s) { return yield } }
35
+ cls = safe(val, :class) { nil }
36
+ typ = safe(cls, :name) { safe(cls, :to_s) { nil } }
37
+
38
+ scalar = safe(-> { Scalar === val }, :call) { true }
39
+ return str, typ, [], [] if scalar
40
+
41
+ named = safe(val, :instance_variables) { [] }
42
+ named += safe(val, :class_variables) { [] }
43
+ # named += safe(val, :constants) { [] }
44
+
45
+ indexed = safe(-> {
46
+ return (0...val.size).to_a if val.is_a?(Array)
47
+ return val.keys if val.respond_to?(:keys) && val.respond_to?(:[])
48
+ []
49
+ }, :call) { [] }
50
+
51
+ return str, typ, named, indexed
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,93 @@
1
+ module Byebug
2
+ module DAP
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(&block)
24
+ @started = false
25
+ if block_given?
26
+ @on_start = block
27
+ @mu = Mutex.new
28
+ @cond = ConditionVariable.new
29
+ end
30
+ end
31
+
32
+ def start(host, port = 0)
33
+ case host
34
+ when :stdio
35
+ start_stdio
36
+ when :unix
37
+ start_unix port
38
+ else
39
+ start_tcp host, port
40
+ end
41
+ end
42
+
43
+ def start_tcp(host, port)
44
+ return if @started
45
+ @started = true
46
+
47
+ launch TCPServer.new(host, port)
48
+ end
49
+
50
+ def start_unix(socket)
51
+ return if @started
52
+ @started = true
53
+
54
+ launch UNIXServer.new(socket)
55
+ end
56
+
57
+ def start_stdio
58
+ return if @started
59
+ @started = true
60
+
61
+ launch STDIO.new
62
+ end
63
+
64
+ private
65
+
66
+ def launch(server)
67
+ DebugThread.new do
68
+ if server.respond_to?(:accept)
69
+ while session = server.accept
70
+ debug session
71
+ end
72
+ else
73
+ debug server
74
+ end
75
+ end
76
+
77
+ return unless defined?(@on_start)
78
+
79
+ @mu.synchronize { @cond.wait(@mu) }
80
+
81
+ @on_start.call
82
+ end
83
+
84
+ def debug(session)
85
+ Context.interface = Byebug::DAP::Interface.new(session)
86
+ Context.processor = Byebug::DAP::CommandProcessor
87
+
88
+ signal_start = ->(_) { @mu.synchronize { @cond.signal } } if defined?(@on_start)
89
+ Byebug::DAP::Controller.new(Context.interface, signal_start).run
90
+ end
91
+ end
92
+ end
93
+ end
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: byebug-dap
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Ethan Reesor
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-10-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: byebug
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '11.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '11.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: ruby-dap
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.1.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.1.0
41
+ description: Implements a Debug Adapter Protocol interface for Byebug
42
+ email: ethan.reesor@gmail.com
43
+ executables:
44
+ - byebug-dap
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - AUTHORS
49
+ - LICENSE
50
+ - bin/byebug-dap
51
+ - lib/byebug/dap.rb
52
+ - lib/byebug/dap/channel.rb
53
+ - lib/byebug/dap/command_processor.rb
54
+ - lib/byebug/dap/controller.rb
55
+ - lib/byebug/dap/handles.rb
56
+ - lib/byebug/dap/interface.rb
57
+ - lib/byebug/dap/invalid_request_argument_error.rb
58
+ - lib/byebug/dap/safe_helpers.rb
59
+ - lib/byebug/dap/server.rb
60
+ homepage: https://gitlab.com/firelizzard/byebug-dap
61
+ licenses:
62
+ - Apache-2.0
63
+ metadata: {}
64
+ post_install_message:
65
+ rdoc_options: []
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ requirements: []
79
+ rubygems_version: 3.0.3
80
+ signing_key:
81
+ specification_version: 4
82
+ summary: Debug Adapter Protocol for Byebug
83
+ test_files: []