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,178 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Megrez
|
|
4
|
+
module Testing
|
|
5
|
+
class FakeAdapter
|
|
6
|
+
DEFAULT_CAPABILITIES = {
|
|
7
|
+
"supportsConfigurationDoneRequest" => true,
|
|
8
|
+
"supportsConditionalBreakpoints" => true,
|
|
9
|
+
"supportsLogPoints" => true,
|
|
10
|
+
"supportsSetVariable" => true,
|
|
11
|
+
"supportsSetExpression" => true,
|
|
12
|
+
"supportsCompletionsRequest" => true,
|
|
13
|
+
"supportsCancelRequest" => true
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
attr_reader :messages
|
|
17
|
+
|
|
18
|
+
def self.command
|
|
19
|
+
root = File.expand_path("../..", __dir__)
|
|
20
|
+
[RbConfig.ruby, "-I#{root}", "-rmegrez/testing", "-e", "Megrez::Testing::FakeAdapter.run_stdio"]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.run_stdio = new.serve(STDIN, STDOUT)
|
|
24
|
+
|
|
25
|
+
def initialize(responses: {}, capabilities: DEFAULT_CAPABILITIES, variable_count: 3)
|
|
26
|
+
raise ArgumentError, "responses must be a Hash" unless responses.is_a?(Hash)
|
|
27
|
+
raise ArgumentError, "capabilities must be a Hash" unless capabilities.is_a?(Hash)
|
|
28
|
+
raise ArgumentError, "variable_count must be nonnegative" unless variable_count.is_a?(Integer) && variable_count >= 0
|
|
29
|
+
|
|
30
|
+
@responses = responses.transform_keys(&:to_s)
|
|
31
|
+
@capabilities = capabilities
|
|
32
|
+
@variable_count = variable_count
|
|
33
|
+
@messages = []
|
|
34
|
+
@sequence = 0
|
|
35
|
+
@lock = Mutex.new
|
|
36
|
+
@sinks = []
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def transport(&receive) = FakeTransport.new(self, &receive)
|
|
40
|
+
|
|
41
|
+
def attach(&sink)
|
|
42
|
+
@lock.synchronize { @sinks << sink }
|
|
43
|
+
sink
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def detach(sink) = @lock.synchronize { @sinks.delete(sink) }
|
|
47
|
+
|
|
48
|
+
def emit(event, body = {})
|
|
49
|
+
message = event_message(event, body)
|
|
50
|
+
@lock.synchronize { @sinks.dup }.each { |sink| sink.call([message]) }
|
|
51
|
+
message
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def request_client(command, arguments = {})
|
|
55
|
+
message = {
|
|
56
|
+
"seq" => sequence,
|
|
57
|
+
"type" => "request",
|
|
58
|
+
"command" => command.to_s,
|
|
59
|
+
"arguments" => arguments
|
|
60
|
+
}
|
|
61
|
+
@lock.synchronize { @sinks.dup }.each { |sink| sink.call([message]) }
|
|
62
|
+
message
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def dispatch(message)
|
|
66
|
+
Protocol.validate_message(message)
|
|
67
|
+
@lock.synchronize { @messages << Protocol.deep_freeze(message.dup) }
|
|
68
|
+
return [] unless message["type"] == "request"
|
|
69
|
+
return [] if message["command"] == "never"
|
|
70
|
+
|
|
71
|
+
override = @responses[message["command"]]
|
|
72
|
+
return Array(override.call(message["arguments"] || {}, message)) if override.respond_to?(:call)
|
|
73
|
+
return [response(message, override)] if @responses.key?(message["command"])
|
|
74
|
+
|
|
75
|
+
result, events = response_for(message)
|
|
76
|
+
[response(message, result), *events]
|
|
77
|
+
rescue AdapterError => error
|
|
78
|
+
[response(message, {}, success: false, message: error.message)]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def serve(input, output = input)
|
|
82
|
+
input.binmode
|
|
83
|
+
output.binmode
|
|
84
|
+
output.sync = true
|
|
85
|
+
while (message = Transport.read_message(input))
|
|
86
|
+
dispatch(message).each { |item| output.write(Transport.frame(item)) }
|
|
87
|
+
end
|
|
88
|
+
rescue IOError, Errno::EPIPE
|
|
89
|
+
nil
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def response_for(message)
|
|
95
|
+
arguments = message["arguments"] || {}
|
|
96
|
+
case message["command"]
|
|
97
|
+
when "initialize"
|
|
98
|
+
[@capabilities, []]
|
|
99
|
+
when "launch", "attach"
|
|
100
|
+
[{}, [event_message("initialized")]]
|
|
101
|
+
when "configurationDone"
|
|
102
|
+
[{}, [stopped_event]]
|
|
103
|
+
when "setBreakpoints"
|
|
104
|
+
values = arguments.fetch("breakpoints", []).each_with_index.map do |breakpoint, index|
|
|
105
|
+
{
|
|
106
|
+
"id" => index + 1,
|
|
107
|
+
"verified" => true,
|
|
108
|
+
"source" => arguments["source"],
|
|
109
|
+
"line" => breakpoint["line"],
|
|
110
|
+
"column" => breakpoint["column"]
|
|
111
|
+
}.compact
|
|
112
|
+
end
|
|
113
|
+
[{"breakpoints" => values}, []]
|
|
114
|
+
when "setFunctionBreakpoints", "setDataBreakpoints"
|
|
115
|
+
values = arguments.fetch("breakpoints", []).each_index.map { |index| {"id" => index + 1, "verified" => true} }
|
|
116
|
+
[{"breakpoints" => values}, []]
|
|
117
|
+
when "setExceptionBreakpoints"
|
|
118
|
+
values = arguments.fetch("filters", []).each_index.map { |index| {"id" => index + 1, "verified" => true} }
|
|
119
|
+
[{"breakpoints" => values}, []]
|
|
120
|
+
when "threads"
|
|
121
|
+
[{"threads" => [{"id" => 1, "name" => "main"}]}, []]
|
|
122
|
+
when "stackTrace"
|
|
123
|
+
[{"stackFrames" => [{"id" => 10, "name" => "main", "source" => {"path" => "app.rb"}, "line" => 1, "column" => 1}], "totalFrames" => 1}, []]
|
|
124
|
+
when "scopes"
|
|
125
|
+
[{"scopes" => [{"name" => "Locals", "variablesReference" => 1, "expensive" => false}]}, []]
|
|
126
|
+
when "variables"
|
|
127
|
+
values = Array.new(@variable_count) do |index|
|
|
128
|
+
{"name" => "value#{index}", "value" => index.to_s, "type" => "Integer", "variablesReference" => 0}
|
|
129
|
+
end
|
|
130
|
+
[{"variables" => values}, []]
|
|
131
|
+
when "evaluate"
|
|
132
|
+
[{"result" => "42", "type" => "Integer", "variablesReference" => 0}, []]
|
|
133
|
+
when "setVariable", "setExpression"
|
|
134
|
+
[{"value" => arguments.fetch("value"), "type" => "String", "variablesReference" => 0}, []]
|
|
135
|
+
when "completions"
|
|
136
|
+
[{"targets" => [{"label" => "value", "text" => "value", "start" => 0, "length" => arguments.fetch("column", 0)}]}, []]
|
|
137
|
+
when "source"
|
|
138
|
+
[{"content" => "puts :ok\n", "mimeType" => "text/x-ruby"}, []]
|
|
139
|
+
when "continue"
|
|
140
|
+
[{"allThreadsContinued" => !arguments["singleThread"]}, [event_message("continued", "threadId" => arguments["threadId"], "allThreadsContinued" => !arguments["singleThread"])] ]
|
|
141
|
+
when "next", "stepIn", "stepOut", "restartFrame"
|
|
142
|
+
[{}, [event_message("continued", "threadId" => arguments["threadId"] || 1), stopped_event]]
|
|
143
|
+
when "restart"
|
|
144
|
+
[{}, [event_message("continued", "threadId" => 1), stopped_event]]
|
|
145
|
+
when "pause"
|
|
146
|
+
[{}, [stopped_event]]
|
|
147
|
+
when "terminate"
|
|
148
|
+
[{}, [event_message("terminated")]]
|
|
149
|
+
when "disconnect"
|
|
150
|
+
[{}, [event_message("terminated")]]
|
|
151
|
+
else
|
|
152
|
+
[@responses.fetch(message["command"], {}), []]
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def response(request, body, success: true, message: nil)
|
|
157
|
+
value = {
|
|
158
|
+
"seq" => sequence,
|
|
159
|
+
"type" => "response",
|
|
160
|
+
"request_seq" => request["seq"],
|
|
161
|
+
"success" => success,
|
|
162
|
+
"command" => request["command"],
|
|
163
|
+
"body" => body
|
|
164
|
+
}
|
|
165
|
+
value["message"] = message if message
|
|
166
|
+
value
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def stopped_event = event_message("stopped", "reason" => "breakpoint", "threadId" => 1, "allThreadsStopped" => true)
|
|
170
|
+
|
|
171
|
+
def event_message(event, body = {})
|
|
172
|
+
{"seq" => sequence, "type" => "event", "event" => event, "body" => body}
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def sequence = @lock.synchronize { @sequence += 1 }
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Megrez
|
|
4
|
+
module Testing
|
|
5
|
+
class FakeTransport < Transport
|
|
6
|
+
STOP = Object.new.freeze
|
|
7
|
+
private_constant :STOP
|
|
8
|
+
|
|
9
|
+
attr_reader :stderr_lines, :pid
|
|
10
|
+
|
|
11
|
+
def initialize(adapter, &receive)
|
|
12
|
+
@adapter = adapter
|
|
13
|
+
@receive = receive
|
|
14
|
+
@stderr_lines = []
|
|
15
|
+
@pid = Process.pid
|
|
16
|
+
@lock = Mutex.new
|
|
17
|
+
@write_lock = Mutex.new
|
|
18
|
+
@responses = Queue.new
|
|
19
|
+
@alive = true
|
|
20
|
+
@sink = @adapter.attach { |messages| enqueue(messages) }
|
|
21
|
+
@reader = Thread.new { read }
|
|
22
|
+
@reader.report_on_exception = false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def listen(&receive)
|
|
26
|
+
raise ArgumentError, "receiver required" unless receive
|
|
27
|
+
raise Error, "debug adapter transport is already listening" if @receive
|
|
28
|
+
|
|
29
|
+
@receive = receive
|
|
30
|
+
self
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def write(message)
|
|
34
|
+
message = wire(message)
|
|
35
|
+
responses = @write_lock.synchronize do
|
|
36
|
+
raise Error, "debug adapter write failed: closed transport" unless alive?
|
|
37
|
+
|
|
38
|
+
@adapter.dispatch(message)
|
|
39
|
+
end
|
|
40
|
+
enqueue(responses)
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def alive? = @lock.synchronize { @alive }
|
|
45
|
+
|
|
46
|
+
def close
|
|
47
|
+
reader = @lock.synchronize do
|
|
48
|
+
if @alive
|
|
49
|
+
@alive = false
|
|
50
|
+
@responses << STOP
|
|
51
|
+
end
|
|
52
|
+
@reader
|
|
53
|
+
end
|
|
54
|
+
@adapter.detach(@sink)
|
|
55
|
+
return if reader == Thread.current
|
|
56
|
+
|
|
57
|
+
reader.kill unless reader.join(1)
|
|
58
|
+
reader.join
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def wire(message)
|
|
65
|
+
frame = Transport.frame(message)
|
|
66
|
+
Transport.read_message(StringIO.new(frame))
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def enqueue(messages)
|
|
70
|
+
values = messages.map { |message| wire(message) }
|
|
71
|
+
@lock.synchronize do
|
|
72
|
+
return unless @alive
|
|
73
|
+
|
|
74
|
+
values.each { |message| @responses << message }
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def read
|
|
79
|
+
loop do
|
|
80
|
+
message = @responses.pop
|
|
81
|
+
break if message.equal?(STOP)
|
|
82
|
+
|
|
83
|
+
@receive.call(message, nil)
|
|
84
|
+
end
|
|
85
|
+
rescue StandardError => error
|
|
86
|
+
@receive.call(nil, error) if alive?
|
|
87
|
+
ensure
|
|
88
|
+
@lock.synchronize { @alive = false }
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Megrez
|
|
6
|
+
class Transport
|
|
7
|
+
def self.read_message(io)
|
|
8
|
+
headers = {}
|
|
9
|
+
bytes = 0
|
|
10
|
+
loop do
|
|
11
|
+
line = io.gets("\r\n", MAX_HEADER_LINE + 1)
|
|
12
|
+
return nil if line.nil? && headers.empty?
|
|
13
|
+
valid = line && line.end_with?("\r\n") && line.bytesize <= MAX_HEADER_LINE
|
|
14
|
+
raise Error, "truncated or oversized DAP header" unless valid
|
|
15
|
+
break if line == "\r\n"
|
|
16
|
+
|
|
17
|
+
bytes += line.bytesize
|
|
18
|
+
raise Error, "oversized DAP headers" if bytes > MAX_HEADERS
|
|
19
|
+
raise Error, "non-ASCII DAP header" unless line.ascii_only?
|
|
20
|
+
|
|
21
|
+
key, value = line.strip.split(":", 2)
|
|
22
|
+
raise Error, "invalid DAP header" unless value && key.match?(/\A[A-Za-z][A-Za-z0-9-]*\z/)
|
|
23
|
+
|
|
24
|
+
key = key.downcase
|
|
25
|
+
raise Error, "duplicate DAP header" if headers.key?(key)
|
|
26
|
+
|
|
27
|
+
headers[key] = value.strip
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
raw_length = headers["content-length"]
|
|
31
|
+
raise Error, "missing or invalid Content-Length" unless raw_length&.match?(/\A\d+\z/)
|
|
32
|
+
|
|
33
|
+
length = Integer(raw_length, 10)
|
|
34
|
+
raise Error, "oversized DAP message" unless length.between?(1, MAX_MESSAGE)
|
|
35
|
+
|
|
36
|
+
charset = headers["content-type"]&.match(/charset\s*=\s*"?([^;"\s]+)/i)&.[](1)
|
|
37
|
+
raise Error, "unsupported DAP character encoding" if charset && !%w[utf-8 utf8].include?(charset.downcase)
|
|
38
|
+
|
|
39
|
+
body = io.read(length)
|
|
40
|
+
raise Error, "truncated DAP body" unless body && body.bytesize == length
|
|
41
|
+
|
|
42
|
+
body.force_encoding(Encoding::UTF_8)
|
|
43
|
+
raise Error, "invalid DAP UTF-8 body" unless body.valid_encoding?
|
|
44
|
+
|
|
45
|
+
Protocol.validate_message(JSON.parse(body))
|
|
46
|
+
rescue JSON::ParserError => error
|
|
47
|
+
raise Error, "invalid DAP JSON: #{error.message.byteslice(0, 256)}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.frame(message)
|
|
51
|
+
raise Error, "DAP message must be an object" unless message.is_a?(Hash)
|
|
52
|
+
|
|
53
|
+
normalized = JSON.parse(JSON.generate(message))
|
|
54
|
+
Protocol.validate_message(normalized)
|
|
55
|
+
body = JSON.generate(normalized).b
|
|
56
|
+
raise Error, "oversized DAP message" unless body.bytesize.between?(1, MAX_MESSAGE)
|
|
57
|
+
|
|
58
|
+
"Content-Length: #{body.bytesize}\r\n\r\n".b + body
|
|
59
|
+
rescue JSON::GeneratorError, JSON::NestingError => error
|
|
60
|
+
raise Error, "invalid DAP JSON: #{error.message.byteslice(0, 256)}"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "socket"
|
|
5
|
+
|
|
6
|
+
module Megrez
|
|
7
|
+
class Transport
|
|
8
|
+
MAX_MESSAGE = 32 << 20
|
|
9
|
+
MAX_HEADER_LINE = 8192
|
|
10
|
+
MAX_HEADERS = 16_384
|
|
11
|
+
|
|
12
|
+
attr_reader :stderr_lines, :pid
|
|
13
|
+
|
|
14
|
+
def self.stdio(command:, env: {}, cwd: nil)
|
|
15
|
+
valid = command.is_a?(Array) && !command.empty? && command.all? do |part|
|
|
16
|
+
part.is_a?(String) && !part.empty? && !part.include?("\0")
|
|
17
|
+
end
|
|
18
|
+
raise ArgumentError, "command must be a nonempty argument array" unless valid
|
|
19
|
+
raise ArgumentError, "environment must be a string map" unless valid_environment?(env)
|
|
20
|
+
raise ArgumentError, "cwd must be a path string" unless cwd.nil? || (cwd.is_a?(String) && !cwd.include?("\0"))
|
|
21
|
+
|
|
22
|
+
options = cwd ? {chdir: cwd} : {}
|
|
23
|
+
input, output, error, process = Open3.popen3(env, *command, **options)
|
|
24
|
+
new(output, input, process: process, stderr: error, closeables: [input, output, error])
|
|
25
|
+
rescue SystemCallError => error
|
|
26
|
+
raise Error, "debug adapter spawn failed: #{error.message}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.tcp(host:, port:, connect_timeout: 10)
|
|
30
|
+
valid_host = host.is_a?(String) && !host.empty? && !host.include?("\0")
|
|
31
|
+
valid_port = port.is_a?(Integer) && port.between?(1, 65_535)
|
|
32
|
+
valid_timeout = connect_timeout.is_a?(Numeric) && connect_timeout.finite? && connect_timeout.positive?
|
|
33
|
+
raise ArgumentError, "host must be a nonempty string" unless valid_host
|
|
34
|
+
raise ArgumentError, "port must be between 1 and 65535" unless valid_port
|
|
35
|
+
raise ArgumentError, "connect_timeout must be positive and finite" unless valid_timeout
|
|
36
|
+
|
|
37
|
+
socket = Socket.tcp(host, port, connect_timeout: connect_timeout)
|
|
38
|
+
socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
|
39
|
+
new(socket, socket, closeables: [socket])
|
|
40
|
+
rescue SystemCallError, SocketError => error
|
|
41
|
+
raise Error, "debug adapter connection failed: #{error.message}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.valid_environment?(env)
|
|
45
|
+
env.is_a?(Hash) && env.all? do |key, value|
|
|
46
|
+
key.is_a?(String) && !key.empty? && !key.include?("\0") && !key.include?("=") &&
|
|
47
|
+
(value.nil? || (value.is_a?(String) && !value.include?("\0")))
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def initialize(input, output, process: nil, stderr: nil, closeables: [])
|
|
52
|
+
@input = input
|
|
53
|
+
@output = output
|
|
54
|
+
@process = process
|
|
55
|
+
@stderr = stderr
|
|
56
|
+
@closeables = closeables.uniq
|
|
57
|
+
@pid = process&.pid
|
|
58
|
+
@stderr_lines = []
|
|
59
|
+
@write_lock = Mutex.new
|
|
60
|
+
@close_lock = Mutex.new
|
|
61
|
+
@closing = false
|
|
62
|
+
[@input, @output, @stderr].compact.uniq.each(&:binmode)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def listen(&receive)
|
|
66
|
+
raise ArgumentError, "receiver required" unless receive
|
|
67
|
+
|
|
68
|
+
@close_lock.synchronize do
|
|
69
|
+
raise Error, "debug adapter transport is closed" if @closing
|
|
70
|
+
raise Error, "debug adapter transport is already listening" if @reader
|
|
71
|
+
|
|
72
|
+
@reader = Thread.new { read(receive) }
|
|
73
|
+
@logger = Thread.new { read_stderr } if @stderr
|
|
74
|
+
[@reader, @logger].compact.each { |thread| thread.report_on_exception = false }
|
|
75
|
+
end
|
|
76
|
+
self
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def write(message)
|
|
80
|
+
frame = self.class.frame(message)
|
|
81
|
+
@write_lock.synchronize do
|
|
82
|
+
raise Error, "debug adapter write failed: closed transport" unless alive?
|
|
83
|
+
|
|
84
|
+
@output.write(frame)
|
|
85
|
+
@output.flush
|
|
86
|
+
end
|
|
87
|
+
nil
|
|
88
|
+
rescue IOError, SystemCallError => error
|
|
89
|
+
raise Error, "debug adapter write failed: #{error.message}"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def alive?
|
|
93
|
+
@close_lock.synchronize do
|
|
94
|
+
!@closing && (@process ? @process.alive? : !@input.closed? && !@output.closed?)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def close
|
|
99
|
+
threads = @close_lock.synchronize do
|
|
100
|
+
next if @closing
|
|
101
|
+
|
|
102
|
+
@closing = true
|
|
103
|
+
[@reader, @logger]
|
|
104
|
+
end
|
|
105
|
+
return unless threads
|
|
106
|
+
|
|
107
|
+
begin
|
|
108
|
+
begin
|
|
109
|
+
@output.shutdown(Socket::SHUT_RDWR) if @output.is_a?(BasicSocket)
|
|
110
|
+
rescue IOError, SystemCallError
|
|
111
|
+
nil
|
|
112
|
+
end
|
|
113
|
+
@output.close unless @output.closed?
|
|
114
|
+
stop_process
|
|
115
|
+
ensure
|
|
116
|
+
@closeables.each do |io|
|
|
117
|
+
io.close unless io.closed?
|
|
118
|
+
rescue IOError
|
|
119
|
+
nil
|
|
120
|
+
end
|
|
121
|
+
threads.compact.each do |thread|
|
|
122
|
+
next if thread == Thread.current
|
|
123
|
+
|
|
124
|
+
thread.kill unless thread.join(1)
|
|
125
|
+
thread.join(1)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
nil
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
def read(receive)
|
|
134
|
+
while (message = self.class.read_message(@input))
|
|
135
|
+
receive.call(message, nil)
|
|
136
|
+
end
|
|
137
|
+
receive.call(nil, Error.new("debug adapter closed its connection")) unless closing?
|
|
138
|
+
rescue StandardError => error
|
|
139
|
+
begin
|
|
140
|
+
receive.call(nil, error) unless closing?
|
|
141
|
+
rescue StandardError
|
|
142
|
+
nil
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def read_stderr
|
|
147
|
+
while (line = @stderr.gets("\n", MAX_HEADER_LINE))
|
|
148
|
+
@stderr_lines << line.scrub.byteslice(0, MAX_HEADER_LINE).scrub("")
|
|
149
|
+
@stderr_lines.shift if @stderr_lines.length > 200
|
|
150
|
+
end
|
|
151
|
+
rescue IOError
|
|
152
|
+
nil
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def closing? = @close_lock.synchronize { @closing }
|
|
156
|
+
|
|
157
|
+
def stop_process
|
|
158
|
+
return unless @process
|
|
159
|
+
return if @process.join(1)
|
|
160
|
+
|
|
161
|
+
Process.kill("TERM", @process.pid)
|
|
162
|
+
return if @process.join(1)
|
|
163
|
+
|
|
164
|
+
Process.kill("KILL", @process.pid)
|
|
165
|
+
@process.join(1)
|
|
166
|
+
rescue Errno::ESRCH, Errno::ECHILD
|
|
167
|
+
nil
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
require_relative "transport/framing"
|
data/lib/megrez.rb
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "megrez/version"
|
|
4
|
+
|
|
5
|
+
module Megrez
|
|
6
|
+
class Error < StandardError; end
|
|
7
|
+
class Timeout < Error; end
|
|
8
|
+
class Cancelled < Error; end
|
|
9
|
+
|
|
10
|
+
class AdapterError < Error
|
|
11
|
+
attr_reader :command, :body
|
|
12
|
+
|
|
13
|
+
def initialize(command, message, body = nil)
|
|
14
|
+
@command = command
|
|
15
|
+
@body = body
|
|
16
|
+
super(message)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
module Value
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
def define(*members)
|
|
24
|
+
return Data.define(*members) if defined?(Data)
|
|
25
|
+
|
|
26
|
+
Struct.new(*members) do
|
|
27
|
+
members.each { |member| undef_method("#{member}=") }
|
|
28
|
+
|
|
29
|
+
define_method(:initialize) do |*values, **keywords|
|
|
30
|
+
if keywords.empty?
|
|
31
|
+
raise ArgumentError, "wrong number of arguments" unless values.length == self.class.members.length
|
|
32
|
+
|
|
33
|
+
super(*values)
|
|
34
|
+
else
|
|
35
|
+
raise ArgumentError, "cannot mix positional and keyword arguments" unless values.empty?
|
|
36
|
+
|
|
37
|
+
missing = self.class.members - keywords.keys
|
|
38
|
+
unknown = keywords.keys - self.class.members
|
|
39
|
+
raise ArgumentError, "missing keyword: #{missing.first.inspect}" unless missing.empty?
|
|
40
|
+
raise ArgumentError, "unknown keyword: #{unknown.first.inspect}" unless unknown.empty?
|
|
41
|
+
|
|
42
|
+
super(*self.class.members.map { |member| keywords.fetch(member) })
|
|
43
|
+
end
|
|
44
|
+
freeze
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
define_method(:with) do |**changes|
|
|
48
|
+
return self if changes.empty?
|
|
49
|
+
|
|
50
|
+
unknown = changes.keys - self.class.members
|
|
51
|
+
raise ArgumentError, "unknown keyword: #{unknown.first.inspect}" unless unknown.empty?
|
|
52
|
+
|
|
53
|
+
self.class.new(**to_h.merge(changes))
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
Breakpoint = Value.define(:id, :verified, :source, :line, :column, :message)
|
|
60
|
+
SourceBreakpoint = Value.define(:line, :column, :condition, :hit_condition, :log_message)
|
|
61
|
+
StackFrame = Value.define(:id, :name, :source, :line, :column, :presentation_hint)
|
|
62
|
+
Scope = Value.define(:name, :variables_reference, :expensive, :presentation_hint)
|
|
63
|
+
Variable = Value.define(:name, :value, :type, :variables_reference,
|
|
64
|
+
:named_count, :indexed_count, :memory_reference)
|
|
65
|
+
ThreadInfo = Value.define(:id, :name)
|
|
66
|
+
private_constant :Value
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
require_relative "megrez/protocol"
|
|
70
|
+
require_relative "megrez/results"
|
|
71
|
+
require_relative "megrez/future"
|
|
72
|
+
require_relative "megrez/transport"
|
|
73
|
+
require_relative "megrez/session"
|