megrez 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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +75 -0
- data/docs/adr/000-template.md +16 -0
- data/docs/adr/001-generation-safe-references.md +20 -0
- data/docs/adr/README.md +3 -0
- data/lib/megrez/future.rb +111 -0
- data/lib/megrez/protocol.rb +133 -0
- data/lib/megrez/results.rb +152 -0
- data/lib/megrez/session/inspection.rb +108 -0
- data/lib/megrez/session/messages.rb +185 -0
- data/lib/megrez/session/requests.rb +143 -0
- data/lib/megrez/session.rb +200 -0
- data/lib/megrez/testing/fake_adapter.rb +178 -0
- data/lib/megrez/testing/fake_transport.rb +92 -0
- data/lib/megrez/testing.rb +7 -0
- data/lib/megrez/transport/framing.rb +63 -0
- data/lib/megrez/transport.rb +172 -0
- data/lib/megrez/version.rb +5 -0
- data/lib/megrez.rb +73 -0
- data/sig/megrez.rbs +208 -0
- metadata +64 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Megrez
|
|
4
|
+
class Session
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
def cancel_request(request_id)
|
|
8
|
+
message = @lock.synchronize do
|
|
9
|
+
next unless @pending.delete(request_id)
|
|
10
|
+
next if @closing
|
|
11
|
+
|
|
12
|
+
sequence = next_sequence
|
|
13
|
+
@ignored_responses[request_id] = true
|
|
14
|
+
@ignored_responses[sequence] = true
|
|
15
|
+
@ignored_responses.shift while @ignored_responses.length > MAX_PENDING * 2
|
|
16
|
+
{seq: sequence, type: "request", command: "cancel", arguments: {requestId: request_id}}
|
|
17
|
+
end
|
|
18
|
+
@transport.write(message) if message
|
|
19
|
+
rescue Error => error
|
|
20
|
+
record_error(error)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def next_sequence
|
|
24
|
+
@sequence += 1
|
|
25
|
+
@sequence = 1 if @sequence > 0x7fff_ffff
|
|
26
|
+
@sequence
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def enqueue(message, error)
|
|
30
|
+
return if @lock.synchronize { @closing }
|
|
31
|
+
|
|
32
|
+
return process_message(message, error) if error || message["type"] == "response"
|
|
33
|
+
|
|
34
|
+
@inbound << [message, error]
|
|
35
|
+
rescue StandardError => failure
|
|
36
|
+
record_error(failure)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def dispatch_messages
|
|
40
|
+
loop do
|
|
41
|
+
item = @inbound.pop
|
|
42
|
+
break if item.equal?(STOP)
|
|
43
|
+
|
|
44
|
+
done = Queue.new
|
|
45
|
+
begin
|
|
46
|
+
@dispatch.call do
|
|
47
|
+
begin
|
|
48
|
+
next if @lock.synchronize { @closing }
|
|
49
|
+
|
|
50
|
+
process_message(*item)
|
|
51
|
+
rescue StandardError => error
|
|
52
|
+
record_error(error)
|
|
53
|
+
ensure
|
|
54
|
+
done << true
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
done.pop
|
|
58
|
+
rescue StandardError => error
|
|
59
|
+
record_error(error)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def process_message(message, error)
|
|
65
|
+
return fail_connection(error) if error
|
|
66
|
+
|
|
67
|
+
Protocol.validate_message(message)
|
|
68
|
+
case message["type"]
|
|
69
|
+
when "response" then process_response(message)
|
|
70
|
+
when "event" then process_event(message)
|
|
71
|
+
when "request" then process_reverse_request(message)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def process_response(message)
|
|
76
|
+
ignored = false
|
|
77
|
+
pending = @lock.synchronize do
|
|
78
|
+
request_sequence = message["request_seq"]
|
|
79
|
+
if @ignored_responses.delete(request_sequence)
|
|
80
|
+
ignored = true
|
|
81
|
+
next
|
|
82
|
+
end
|
|
83
|
+
@pending.delete(request_sequence)
|
|
84
|
+
end
|
|
85
|
+
return if ignored
|
|
86
|
+
return record_error(Error.new("unsolicited DAP response")) unless pending
|
|
87
|
+
return pending.future.fulfill(error: Error.new("mismatched DAP response command")) if message["command"] != pending.command
|
|
88
|
+
|
|
89
|
+
unless message["success"]
|
|
90
|
+
text = message["message"] || "debug adapter rejected #{pending.command}"
|
|
91
|
+
body = message["body"]&.then { |value| Protocol.deep_freeze(value.dup) }
|
|
92
|
+
return pending.future.fulfill(error: AdapterError.new(pending.command, text, body))
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
body = message.fetch("body", {})
|
|
96
|
+
value = pending.validate ? pending.validate.call(body) : body
|
|
97
|
+
pending.future.fulfill(value)
|
|
98
|
+
rescue StandardError => error
|
|
99
|
+
pending&.future&.fulfill(error: error)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def process_event(message)
|
|
103
|
+
key = event_key(message["event"])
|
|
104
|
+
body = Protocol.deep_freeze(message.fetch("body", {}).dup)
|
|
105
|
+
handlers = @lock.synchronize do
|
|
106
|
+
case key
|
|
107
|
+
when :stopped
|
|
108
|
+
@generation += 1
|
|
109
|
+
@state = :stopped
|
|
110
|
+
when :continued
|
|
111
|
+
@state = :running
|
|
112
|
+
when :terminated, :exited
|
|
113
|
+
@generation += 1
|
|
114
|
+
@state = :terminated
|
|
115
|
+
@ended = true
|
|
116
|
+
end
|
|
117
|
+
(@handlers[key] || []).dup
|
|
118
|
+
end
|
|
119
|
+
handlers.each do |handler|
|
|
120
|
+
handler.call(body)
|
|
121
|
+
rescue StandardError => error
|
|
122
|
+
record_error(error)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def process_reverse_request(message)
|
|
127
|
+
command = message["command"]
|
|
128
|
+
handler = @lock.synchronize { @request_handlers[command] }
|
|
129
|
+
raise AdapterError.new(command, "unsupported adapter request: #{command}") unless handler
|
|
130
|
+
|
|
131
|
+
result = handler.call(Protocol.deep_freeze(message.fetch("arguments", {}).dup)) || {}
|
|
132
|
+
Protocol.object(result, "adapter request result")
|
|
133
|
+
Protocol.validate_outbound(result)
|
|
134
|
+
send_response(message, success: true, body: result)
|
|
135
|
+
rescue StandardError => error
|
|
136
|
+
record_error(error)
|
|
137
|
+
send_response(message, success: false, message: error.message.scrub.byteslice(0, 4096).scrub(""))
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def send_response(request, success:, body: nil, message: nil)
|
|
141
|
+
response = @lock.synchronize do
|
|
142
|
+
next if @closing
|
|
143
|
+
|
|
144
|
+
value = {seq: next_sequence, type: "response", request_seq: request["seq"],
|
|
145
|
+
success: success, command: request["command"]}
|
|
146
|
+
value[:body] = body if body
|
|
147
|
+
value[:message] = message if message
|
|
148
|
+
value
|
|
149
|
+
end
|
|
150
|
+
@transport.write(response) if response
|
|
151
|
+
rescue Error => error
|
|
152
|
+
record_error(error)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def fail_connection(error)
|
|
156
|
+
pending = @lock.synchronize do
|
|
157
|
+
next [] if @closing
|
|
158
|
+
|
|
159
|
+
@state = :terminated
|
|
160
|
+
@ended = true
|
|
161
|
+
@generation += 1
|
|
162
|
+
values = @pending.values.map(&:future)
|
|
163
|
+
@pending.clear
|
|
164
|
+
values
|
|
165
|
+
end
|
|
166
|
+
record_error(error)
|
|
167
|
+
pending.each { |future| future.fulfill(error: error) }
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def record_error(error)
|
|
171
|
+
prefix = error.is_a?(Error) ? "" : "#{error.class}: "
|
|
172
|
+
message = "#{prefix}#{error.message}".scrub.byteslice(0, 4096).scrub("")
|
|
173
|
+
bounded = error.is_a?(Error) && error.message.bytesize <= 4096 ? error : Error.new(message)
|
|
174
|
+
@lock.synchronize do
|
|
175
|
+
@errors << bounded
|
|
176
|
+
@errors.shift if @errors.length > MAX_ERRORS
|
|
177
|
+
end
|
|
178
|
+
bounded
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def event_key(event)
|
|
182
|
+
event.to_s.gsub(/([a-z\d])([A-Z])/, '\\1_\\2').downcase.to_sym
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Megrez
|
|
4
|
+
class Session
|
|
5
|
+
def launch(configuration)
|
|
6
|
+
transition_request("launch", configuration, from: :initialized, to: :configuring) { |body| Results.body(body) }
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def attach(configuration)
|
|
10
|
+
transition_request("attach", configuration, from: :initialized, to: :configuring) { |body| Results.body(body) }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def configuration_done
|
|
14
|
+
transition_request("configurationDone", {}, from: :configuring, to: :running) { |body| Results.body(body) }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def disconnect(terminate: false, restart: false)
|
|
18
|
+
Protocol.boolean(terminate, "terminate")
|
|
19
|
+
Protocol.boolean(restart, "restart")
|
|
20
|
+
future = transition_request(
|
|
21
|
+
"disconnect",
|
|
22
|
+
{terminateDebuggee: terminate, restart: restart},
|
|
23
|
+
from: %i[initialized configuring running stopped],
|
|
24
|
+
to: :terminated
|
|
25
|
+
) { |body| Results.body(body) }
|
|
26
|
+
future.then { |_value, _error| close }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def set_breakpoints(source_path, breakpoints)
|
|
30
|
+
source_path = Protocol.string(source_path, "source path", empty: false)
|
|
31
|
+
breakpoints = Protocol.collection(breakpoints, "source breakpoints").map do |breakpoint|
|
|
32
|
+
source_breakpoint(breakpoint)
|
|
33
|
+
end
|
|
34
|
+
send_request(
|
|
35
|
+
"setBreakpoints",
|
|
36
|
+
{source: {path: source_path}, breakpoints: breakpoints},
|
|
37
|
+
states: %i[configuring stopped running]
|
|
38
|
+
) { |body| Results.breakpoints(body) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def set_function_breakpoints(names)
|
|
42
|
+
values = Protocol.collection(names, "function breakpoint names").map do |name|
|
|
43
|
+
{name: Protocol.string(name, "function breakpoint name", empty: false)}
|
|
44
|
+
end
|
|
45
|
+
breakpoint_request("setFunctionBreakpoints", breakpoints: values)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def set_exception_breakpoints(filters, options: [])
|
|
49
|
+
filters = Protocol.collection(filters, "exception filters").map do |filter|
|
|
50
|
+
Protocol.string(filter, "exception filter", empty: false)
|
|
51
|
+
end
|
|
52
|
+
options = object_array(options, "exception options")
|
|
53
|
+
breakpoint_request("setExceptionBreakpoints", filters: filters, filterOptions: options)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def set_data_breakpoints(descriptors)
|
|
57
|
+
values = object_array(descriptors, "data breakpoints")
|
|
58
|
+
values.each do |descriptor|
|
|
59
|
+
Protocol.string(descriptor["dataId"] || descriptor[:dataId], "data breakpoint dataId", empty: false)
|
|
60
|
+
end
|
|
61
|
+
breakpoint_request("setDataBreakpoints", breakpoints: values)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def continue(thread_id, all: false)
|
|
65
|
+
Protocol.boolean(all, "all")
|
|
66
|
+
execution_request("continue", thread_id, singleThread: !all)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def step_over(thread_id, granularity: :statement)
|
|
70
|
+
execution_request("next", thread_id, granularity: granularity_value(granularity))
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def step_in(thread_id, target_id: nil)
|
|
74
|
+
arguments = {}
|
|
75
|
+
arguments[:targetId] = Protocol.integer(target_id, "step target id") unless target_id.nil?
|
|
76
|
+
execution_request("stepIn", thread_id, **arguments)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def step_out(thread_id) = execution_request("stepOut", thread_id)
|
|
80
|
+
|
|
81
|
+
def pause(thread_id)
|
|
82
|
+
thread_id = Protocol.integer(thread_id, "thread id")
|
|
83
|
+
send_request("pause", {threadId: thread_id}, states: [:running]) { |body| Results.body(body) }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def restart_frame(frame_id)
|
|
87
|
+
frame_id = Protocol.integer(frame_id, "frame id")
|
|
88
|
+
transition_request("restartFrame", {frameId: frame_id}, from: :stopped, to: :running) do |body|
|
|
89
|
+
Results.body(body)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def restart(arguments = {})
|
|
94
|
+
transition_request("restart", arguments, from: %i[running stopped], to: :running) { |body| Results.body(body) }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def terminate
|
|
98
|
+
send_request("terminate", {}, states: %i[configuring running stopped]) { |body| Results.body(body) }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def breakpoint_request(command, arguments)
|
|
104
|
+
send_request(command, arguments, states: %i[configuring stopped running]) { |body| Results.breakpoints(body) }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def execution_request(command, thread_id, **arguments)
|
|
108
|
+
thread_id = Protocol.integer(thread_id, "thread id")
|
|
109
|
+
transition_request(command, {threadId: thread_id, **arguments}, from: :stopped, to: :running) do |body|
|
|
110
|
+
Results.body(body)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def source_breakpoint(value)
|
|
115
|
+
raise Error, "source breakpoint must be a SourceBreakpoint" unless value.is_a?(SourceBreakpoint)
|
|
116
|
+
|
|
117
|
+
result = {line: Protocol.uint(value.line, "breakpoint line")}
|
|
118
|
+
result[:column] = Protocol.uint(value.column, "breakpoint column") unless value.column.nil?
|
|
119
|
+
{
|
|
120
|
+
condition: value.condition,
|
|
121
|
+
hitCondition: value.hit_condition,
|
|
122
|
+
logMessage: value.log_message
|
|
123
|
+
}.each do |key, text|
|
|
124
|
+
result[key] = Protocol.string(text, "breakpoint #{key}") unless text.nil?
|
|
125
|
+
end
|
|
126
|
+
result
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def object_array(values, name)
|
|
130
|
+
Protocol.collection(values, name).map do |value|
|
|
131
|
+
Protocol.validate_outbound(Protocol.object(value, name))
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def granularity_value(value)
|
|
136
|
+
value = value.to_s
|
|
137
|
+
raise ArgumentError, "granularity must be statement, line, or instruction" unless %w[statement line instruction].include?(value)
|
|
138
|
+
|
|
139
|
+
value
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Megrez
|
|
4
|
+
class Session
|
|
5
|
+
MAX_PENDING = 1024
|
|
6
|
+
MAX_INBOUND = 1024
|
|
7
|
+
MAX_ERRORS = 100
|
|
8
|
+
STOP = Object.new.freeze
|
|
9
|
+
Pending = Struct.new(:future, :command, :validate, keyword_init: true)
|
|
10
|
+
private_constant :MAX_PENDING, :MAX_INBOUND, :MAX_ERRORS, :STOP, :Pending
|
|
11
|
+
|
|
12
|
+
def self.stdio(command:, env: {}, cwd: nil, dispatch: ->(&block) { block.call })
|
|
13
|
+
new(Transport.stdio(command: command, env: env, cwd: cwd), dispatch: dispatch)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.tcp(host:, port:, dispatch: ->(&block) { block.call }, connect_timeout: 10)
|
|
17
|
+
new(Transport.tcp(host: host, port: port, connect_timeout: connect_timeout), dispatch: dispatch)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
attr_reader :transport
|
|
21
|
+
|
|
22
|
+
def initialize(transport, dispatch: ->(&block) { block.call })
|
|
23
|
+
valid = transport.respond_to?(:listen) && transport.respond_to?(:write) && transport.respond_to?(:close)
|
|
24
|
+
raise ArgumentError, "transport must support listen, write, and close" unless valid
|
|
25
|
+
raise ArgumentError, "dispatch must respond to call" unless dispatch.respond_to?(:call)
|
|
26
|
+
|
|
27
|
+
@transport = transport
|
|
28
|
+
@dispatch = dispatch
|
|
29
|
+
@lock = Mutex.new
|
|
30
|
+
@pending = {}
|
|
31
|
+
@ignored_responses = {}
|
|
32
|
+
@handlers = {}
|
|
33
|
+
@request_handlers = {}
|
|
34
|
+
@errors = []
|
|
35
|
+
@sequence = 0
|
|
36
|
+
@generation = 0
|
|
37
|
+
@state = :initialized
|
|
38
|
+
@started = false
|
|
39
|
+
@closing = false
|
|
40
|
+
@ended = false
|
|
41
|
+
@capabilities = {}.freeze
|
|
42
|
+
@inbound = SizedQueue.new(MAX_INBOUND)
|
|
43
|
+
@dispatcher = Thread.new { dispatch_messages }
|
|
44
|
+
@dispatcher.report_on_exception = false
|
|
45
|
+
@transport.listen { |message, error| enqueue(message, error) }
|
|
46
|
+
rescue StandardError
|
|
47
|
+
close
|
|
48
|
+
raise
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def start(adapter_id:, lines_start_at_1: true, columns_start_at_1: true, path_format: "path", timeout: 10)
|
|
52
|
+
started_now = false
|
|
53
|
+
adapter_id = Protocol.string(adapter_id, "adapter_id", empty: false, max: 256)
|
|
54
|
+
Protocol.boolean(lines_start_at_1, "lines_start_at_1")
|
|
55
|
+
Protocol.boolean(columns_start_at_1, "columns_start_at_1")
|
|
56
|
+
raise ArgumentError, "path_format must be path or uri" unless %w[path uri].include?(path_format)
|
|
57
|
+
|
|
58
|
+
reverse_requests = @lock.synchronize do
|
|
59
|
+
raise Error, "debug session already started" if @started
|
|
60
|
+
raise Error, "debug session is closed" if @closing
|
|
61
|
+
|
|
62
|
+
@started = true
|
|
63
|
+
started_now = true
|
|
64
|
+
@request_handlers.keys
|
|
65
|
+
end
|
|
66
|
+
arguments = {
|
|
67
|
+
clientID: "megrez",
|
|
68
|
+
clientName: "Megrez",
|
|
69
|
+
adapterID: adapter_id,
|
|
70
|
+
linesStartAt1: lines_start_at_1,
|
|
71
|
+
columnsStartAt1: columns_start_at_1,
|
|
72
|
+
pathFormat: path_format,
|
|
73
|
+
supportsVariableType: true,
|
|
74
|
+
supportsVariablePaging: true,
|
|
75
|
+
supportsProgressReporting: true,
|
|
76
|
+
supportsRunInTerminalRequest: reverse_requests.include?("runInTerminal"),
|
|
77
|
+
supportsStartDebuggingRequest: reverse_requests.include?("startDebugging")
|
|
78
|
+
}
|
|
79
|
+
future = send_request("initialize", arguments, states: [:initialized], allow_unstarted: true) do |body|
|
|
80
|
+
Results.capabilities(body)
|
|
81
|
+
end
|
|
82
|
+
value = future.await(timeout: timeout)
|
|
83
|
+
@lock.synchronize { @capabilities = value }
|
|
84
|
+
value
|
|
85
|
+
rescue StandardError
|
|
86
|
+
close if started_now
|
|
87
|
+
raise
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def request(command, arguments = {})
|
|
91
|
+
send_request(command, arguments, states: %i[initialized configuring running stopped]) { |body| Results.body(body) }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def on(event, &handler)
|
|
95
|
+
raise ArgumentError, "handler required" unless handler
|
|
96
|
+
|
|
97
|
+
key = event_key(event)
|
|
98
|
+
@lock.synchronize { (@handlers[key] ||= []) << handler }
|
|
99
|
+
handler
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def on_request(command, &handler)
|
|
103
|
+
raise ArgumentError, "handler required" unless handler
|
|
104
|
+
|
|
105
|
+
command = Protocol.string(command.to_s, "request command", empty: false, max: 256)
|
|
106
|
+
@lock.synchronize { @request_handlers[command] = handler }
|
|
107
|
+
handler
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def capabilities = @lock.synchronize { @capabilities }
|
|
111
|
+
def state = @lock.synchronize { @state }
|
|
112
|
+
def generation = @lock.synchronize { @generation }
|
|
113
|
+
def errors = @lock.synchronize { @errors.dup.freeze }
|
|
114
|
+
|
|
115
|
+
def close
|
|
116
|
+
pending = @lock&.synchronize do
|
|
117
|
+
next if @closing
|
|
118
|
+
|
|
119
|
+
@closing = true
|
|
120
|
+
@state = :terminated
|
|
121
|
+
values = @pending.values.map(&:future)
|
|
122
|
+
@pending.clear
|
|
123
|
+
values
|
|
124
|
+
end
|
|
125
|
+
return nil unless pending
|
|
126
|
+
|
|
127
|
+
error = Error.new("debug session closed")
|
|
128
|
+
pending.each { |future| future.fulfill(error: error) }
|
|
129
|
+
@inbound&.clear
|
|
130
|
+
@inbound&.push(STOP)
|
|
131
|
+
@transport&.close
|
|
132
|
+
if @dispatcher && @dispatcher != Thread.current
|
|
133
|
+
@dispatcher.kill unless @dispatcher.join(1)
|
|
134
|
+
@dispatcher.join
|
|
135
|
+
end
|
|
136
|
+
nil
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
private
|
|
140
|
+
|
|
141
|
+
def send_request(command, arguments, states:, allow_unstarted: false, &validate)
|
|
142
|
+
command = Protocol.string(command.to_s, "DAP command", empty: false, max: 256)
|
|
143
|
+
arguments = Protocol.object(arguments, "DAP request arguments")
|
|
144
|
+
Protocol.validate_outbound(arguments)
|
|
145
|
+
|
|
146
|
+
pending = @lock.synchronize do
|
|
147
|
+
raise Error, "debug session is closed" if @closing
|
|
148
|
+
raise Error, "debug session has not started" unless @started || allow_unstarted
|
|
149
|
+
raise Error, "#{command} is invalid while #{@state}" unless states.include?(@state)
|
|
150
|
+
raise Error, "too many pending DAP requests" if @pending.length >= MAX_PENDING
|
|
151
|
+
|
|
152
|
+
sequence = next_sequence
|
|
153
|
+
future = Future.new(sequence, on_error: method(:record_error)) { |id| cancel_request(id) }
|
|
154
|
+
value = Pending.new(future: future, command: command, validate: validate)
|
|
155
|
+
@pending[sequence] = value
|
|
156
|
+
[sequence, value]
|
|
157
|
+
end
|
|
158
|
+
sequence, value = pending
|
|
159
|
+
@transport.write(seq: sequence, type: "request", command: command, arguments: arguments)
|
|
160
|
+
value.future
|
|
161
|
+
rescue StandardError => error
|
|
162
|
+
if pending
|
|
163
|
+
@lock.synchronize { @pending.delete(pending.first) }
|
|
164
|
+
pending.last.future.fulfill(error: error)
|
|
165
|
+
pending.last.future
|
|
166
|
+
else
|
|
167
|
+
raise
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def transition_request(command, arguments, from:, to:, &validate)
|
|
172
|
+
previous = @lock.synchronize do
|
|
173
|
+
raise Error, "debug session is closed" if @closing
|
|
174
|
+
raise Error, "debug session has not started" unless @started
|
|
175
|
+
raise Error, "#{command} is invalid while #{@state}" unless Array(from).include?(@state)
|
|
176
|
+
|
|
177
|
+
old = @state
|
|
178
|
+
@state = to
|
|
179
|
+
old
|
|
180
|
+
end
|
|
181
|
+
future = send_request(command, arguments, states: [to], &validate)
|
|
182
|
+
future.then do |_value, error|
|
|
183
|
+
@lock.synchronize do
|
|
184
|
+
@state = previous if error && !@closing && !@ended && @state == to
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
future
|
|
188
|
+
rescue StandardError
|
|
189
|
+
@lock.synchronize do
|
|
190
|
+
@state = previous if previous && !@closing && !@ended && @state == to
|
|
191
|
+
end
|
|
192
|
+
raise
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
require_relative "session/messages"
|
|
199
|
+
require_relative "session/requests"
|
|
200
|
+
require_relative "session/inspection"
|