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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 85a6c24732dbeeb7c806452f4d963df6c0e1d4aa33faa2b45961aeb27c737f03
|
|
4
|
+
data.tar.gz: b742be2cdcd1d743185f2fa3f3eabf0224f1bc94f00f08ea6eaaf35631ac5eb6
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 6e1fe32a3fea8f74156ec480ec0fe60c43098b7c5748a3896e6dc13436416d06a4822f87e82b8ecac82698f6b5166c16e2ae01bcb5a1ec331eed586d7e1feed0
|
|
7
|
+
data.tar.gz: 456406256f220f0a3faecb88d8ea49acc07dd15284a3bc1c8e90e2e74888a688798a084609012e2794f833c54826b15239cca1bf22099b30390095be57409f5b
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yudai Takada
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Megrez
|
|
2
|
+
|
|
3
|
+
Megrez is a pure Ruby Debug Adapter Protocol (DAP) client. It owns adapter
|
|
4
|
+
transport, protocol validation, session state, breakpoints, execution control,
|
|
5
|
+
and lazy variable expansion without depending on an editor or UI toolkit.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem "megrez"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
require "megrez"
|
|
17
|
+
|
|
18
|
+
session = Megrez::Session.stdio(command: ["path/to/debug-adapter", "--stdio"])
|
|
19
|
+
session.on(:stopped) { |event| puts "stopped: #{event.fetch("reason")}" }
|
|
20
|
+
initialized = Queue.new
|
|
21
|
+
session.on(:initialized) { initialized << true }
|
|
22
|
+
session.start(adapter_id: "my-adapter")
|
|
23
|
+
launch = session.launch("program" => File.expand_path("app.rb"))
|
|
24
|
+
initialized.pop
|
|
25
|
+
session.set_breakpoints("app.rb", [
|
|
26
|
+
Megrez::SourceBreakpoint.new(
|
|
27
|
+
line: 12, column: nil, condition: nil, hit_condition: nil,
|
|
28
|
+
log_message: nil
|
|
29
|
+
)
|
|
30
|
+
]).await(timeout: 5)
|
|
31
|
+
session.configuration_done.await(timeout: 5)
|
|
32
|
+
launch.await(timeout: 5)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Requests return `Megrez::Future`; call `await(timeout:)` where a blocking
|
|
36
|
+
result is needed. Cancelling a future sends the DAP `cancel` request and
|
|
37
|
+
rejects the local wait. Event callbacks are delivered in adapter arrival order.
|
|
38
|
+
|
|
39
|
+
TCP adapters are supported with `Megrez::Session.tcp(host:, port:)`. Adapter
|
|
40
|
+
reverse requests such as `runInTerminal` and `startDebugging` can be handled
|
|
41
|
+
with `on_request`:
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
session.on_request("startDebugging") { |arguments| start_child(arguments) }
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Register reverse-request handlers before `start`; Megrez advertises only the
|
|
48
|
+
requests that have handlers.
|
|
49
|
+
|
|
50
|
+
Values returned as `variables_reference` embed the current stop generation.
|
|
51
|
+
Passing a value from an earlier stop to `variables` or `set_variable` raises
|
|
52
|
+
`Megrez::Error` instead of querying stale adapter state.
|
|
53
|
+
|
|
54
|
+
## Conformance
|
|
55
|
+
|
|
56
|
+
The bundled fake adapter is exercised by default. Set
|
|
57
|
+
`MEGREZ_ADAPTERS=ruby` to additionally run the installed `rdbg` conformance
|
|
58
|
+
scenario.
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
bundle install
|
|
62
|
+
bundle exec rake test
|
|
63
|
+
bundle exec rbs -I sig -r stringio validate
|
|
64
|
+
BUDGET=1 bundle exec rake bench
|
|
65
|
+
gem build --strict megrez.gemspec
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Adapters are external programs and should be treated as untrusted input.
|
|
69
|
+
Megrez does not invoke a shell, caps frame/header/pending-request sizes, and
|
|
70
|
+
bounds retained stderr and callback errors. TCP is unencrypted; use it only on
|
|
71
|
+
a trusted interface or inside a secure tunnel.
|
|
72
|
+
|
|
73
|
+
## License
|
|
74
|
+
|
|
75
|
+
Megrez is available under the MIT License.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# ADR NNN: Implementation decision title
|
|
2
|
+
|
|
3
|
+
- Status: Proposed
|
|
4
|
+
- Date: YYYY-MM-DD
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
Describe the concrete implementation question and its constraints.
|
|
9
|
+
|
|
10
|
+
## Decision
|
|
11
|
+
|
|
12
|
+
Describe the durable boundary or architecture choice.
|
|
13
|
+
|
|
14
|
+
## Consequences
|
|
15
|
+
|
|
16
|
+
Describe the important trade-offs and when to revisit the decision.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# ADR 001: Encode stop generations in variable references
|
|
2
|
+
|
|
3
|
+
- Status: Accepted
|
|
4
|
+
- Date: 2026-09-15
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
DAP variable references are valid only while execution remains stopped. An
|
|
9
|
+
adapter may reuse the same integer after the next stop, so retaining only a
|
|
10
|
+
set of seen integers cannot reliably identify stale UI state.
|
|
11
|
+
|
|
12
|
+
## Decision
|
|
13
|
+
|
|
14
|
+
Expose zero unchanged and encode the current stop generation with every
|
|
15
|
+
positive adapter reference. Decode and verify it before sending requests.
|
|
16
|
+
|
|
17
|
+
## Consequences
|
|
18
|
+
|
|
19
|
+
References remain integers and stale values are rejected deterministically.
|
|
20
|
+
They are opaque handles; callers must not interpret their numeric value.
|
data/docs/adr/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Megrez
|
|
4
|
+
class Future
|
|
5
|
+
class Subscription
|
|
6
|
+
def initialize(&detach) = @detach = detach
|
|
7
|
+
|
|
8
|
+
def detach
|
|
9
|
+
callback, @detach = @detach, nil
|
|
10
|
+
callback&.call
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
attr_reader :id, :callback_errors
|
|
15
|
+
|
|
16
|
+
def initialize(id, on_error: nil, &cancel)
|
|
17
|
+
@id = id
|
|
18
|
+
@cancel_callback = cancel
|
|
19
|
+
@on_error = on_error
|
|
20
|
+
@lock = Mutex.new
|
|
21
|
+
@ready = ConditionVariable.new
|
|
22
|
+
@callbacks = []
|
|
23
|
+
@callback_errors = []
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def fulfill(value = nil, error: nil)
|
|
27
|
+
callbacks = @lock.synchronize do
|
|
28
|
+
return if @done
|
|
29
|
+
|
|
30
|
+
@value = value
|
|
31
|
+
@error = error
|
|
32
|
+
@done = true
|
|
33
|
+
@ready.broadcast
|
|
34
|
+
saved, @callbacks = @callbacks, []
|
|
35
|
+
saved
|
|
36
|
+
end
|
|
37
|
+
callbacks.each { |callback| invoke(callback, @value, @error) }
|
|
38
|
+
self
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def then(&callback)
|
|
42
|
+
raise ArgumentError, "callback required" unless callback
|
|
43
|
+
|
|
44
|
+
ready = @lock.synchronize do
|
|
45
|
+
@callbacks << callback unless @done
|
|
46
|
+
@done
|
|
47
|
+
end
|
|
48
|
+
invoke(callback, @value, @error) if ready
|
|
49
|
+
self
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def on_complete(&callback)
|
|
53
|
+
raise ArgumentError, "callback required" unless callback
|
|
54
|
+
|
|
55
|
+
self.then(&callback)
|
|
56
|
+
Subscription.new { @lock.synchronize { @callbacks.delete(callback) } }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def done? = @lock.synchronize { !!@done }
|
|
60
|
+
|
|
61
|
+
def await(timeout: nil)
|
|
62
|
+
valid = timeout.nil? || (timeout.is_a?(Numeric) && timeout.finite? && timeout >= 0)
|
|
63
|
+
raise ArgumentError, "timeout must be finite and nonnegative" unless valid
|
|
64
|
+
|
|
65
|
+
deadline = timeout && Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
66
|
+
@lock.synchronize do
|
|
67
|
+
until @done
|
|
68
|
+
remaining = deadline && deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
69
|
+
raise Timeout, "DAP request #{@id} timed out" if remaining && remaining <= 0
|
|
70
|
+
|
|
71
|
+
@ready.wait(@lock, remaining)
|
|
72
|
+
end
|
|
73
|
+
raise @error if @error
|
|
74
|
+
|
|
75
|
+
@value
|
|
76
|
+
end
|
|
77
|
+
rescue Timeout
|
|
78
|
+
cancel
|
|
79
|
+
raise
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def cancel
|
|
83
|
+
callback = @lock.synchronize do
|
|
84
|
+
return false if @done || @cancelling
|
|
85
|
+
|
|
86
|
+
@cancelling = true
|
|
87
|
+
@cancel_callback
|
|
88
|
+
end
|
|
89
|
+
invoke(->(*) { callback.call(@id) }, nil, nil) if callback
|
|
90
|
+
fulfill(error: Cancelled.new("DAP request #{@id} cancelled"))
|
|
91
|
+
true
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def invoke(callback, value, error)
|
|
97
|
+
callback.call(value, error)
|
|
98
|
+
rescue StandardError => callback_error
|
|
99
|
+
bounded = Error.new("#{callback_error.class}: #{callback_error.message}".scrub.byteslice(0, 2048).scrub(""))
|
|
100
|
+
@lock.synchronize do
|
|
101
|
+
@callback_errors << bounded
|
|
102
|
+
@callback_errors.shift if @callback_errors.length > 32
|
|
103
|
+
end
|
|
104
|
+
begin
|
|
105
|
+
@on_error&.call(bounded)
|
|
106
|
+
rescue StandardError
|
|
107
|
+
nil
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Megrez
|
|
4
|
+
module Protocol
|
|
5
|
+
MAX_COLLECTION = 100_000
|
|
6
|
+
MAX_DEPTH = 64
|
|
7
|
+
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def validate_message(message)
|
|
11
|
+
raise Error, "DAP message must be an object" unless message.is_a?(Hash)
|
|
12
|
+
|
|
13
|
+
uint(message["seq"], "DAP seq", positive: true)
|
|
14
|
+
case message["type"]
|
|
15
|
+
when "request" then validate_request(message)
|
|
16
|
+
when "response" then validate_response(message)
|
|
17
|
+
when "event" then validate_event(message)
|
|
18
|
+
else raise Error, "invalid DAP message type"
|
|
19
|
+
end
|
|
20
|
+
message
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def validate_outbound(value)
|
|
24
|
+
validate_json(value, 0)
|
|
25
|
+
value
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def validate_request(message)
|
|
29
|
+
string(message["command"], "DAP command", empty: false, max: 256)
|
|
30
|
+
validate_json(object(message["arguments"], "DAP request arguments"), 0) if message.key?("arguments")
|
|
31
|
+
forbidden = %w[request_seq success event]
|
|
32
|
+
raise Error, "invalid DAP request fields" if forbidden.any? { |key| message.key?(key) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def validate_response(message)
|
|
36
|
+
uint(message["request_seq"], "DAP request_seq", positive: true)
|
|
37
|
+
boolean(message["success"], "DAP response success")
|
|
38
|
+
string(message["command"], "DAP response command", empty: false, max: 256)
|
|
39
|
+
string(message["message"], "DAP response message", max: 4096) if message.key?("message")
|
|
40
|
+
validate_json(object(message["body"], "DAP response body"), 0) if message.key?("body")
|
|
41
|
+
raise Error, "invalid DAP response fields" if message.key?("event")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def validate_event(message)
|
|
45
|
+
string(message["event"], "DAP event", empty: false, max: 256)
|
|
46
|
+
validate_json(object(message["body"], "DAP event body"), 0) if message.key?("body")
|
|
47
|
+
forbidden = %w[request_seq success command]
|
|
48
|
+
raise Error, "invalid DAP event fields" if forbidden.any? { |key| message.key?(key) }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def validate_json(value, depth)
|
|
52
|
+
raise Error, "DAP value is nested too deeply" if depth > MAX_DEPTH
|
|
53
|
+
|
|
54
|
+
case value
|
|
55
|
+
when nil, true, false, Integer, String
|
|
56
|
+
string(value, "DAP string") if value.is_a?(String)
|
|
57
|
+
when Float
|
|
58
|
+
raise Error, "DAP number must be finite" unless value.finite?
|
|
59
|
+
when Array
|
|
60
|
+
collection(value, "DAP array").each { |item| validate_json(item, depth + 1) }
|
|
61
|
+
when Hash
|
|
62
|
+
value.each do |key, item|
|
|
63
|
+
raise Error, "DAP object keys must be strings or symbols" unless key.is_a?(String) || key.is_a?(Symbol)
|
|
64
|
+
|
|
65
|
+
string(key.to_s, "DAP object key", empty: false, max: 1024)
|
|
66
|
+
validate_json(item, depth + 1)
|
|
67
|
+
end
|
|
68
|
+
else
|
|
69
|
+
raise Error, "unsupported DAP value: #{value.class}"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def object(value, name)
|
|
74
|
+
raise Error, "#{name} must be an object" unless value.is_a?(Hash)
|
|
75
|
+
|
|
76
|
+
value
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def collection(value, name)
|
|
80
|
+
raise Error, "#{name} must be an array" unless value.is_a?(Array)
|
|
81
|
+
raise Error, "#{name} has too many elements" if value.length > MAX_COLLECTION
|
|
82
|
+
|
|
83
|
+
value
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def string(value, name, empty: true, max: 1 << 20)
|
|
87
|
+
valid = value.is_a?(String) && value.encoding != Encoding::BINARY && value.valid_encoding? && !value.include?("\0")
|
|
88
|
+
valid &&= !value.empty? unless empty
|
|
89
|
+
valid &&= value.bytesize <= max
|
|
90
|
+
raise Error, "#{name} must be a valid UTF-8 string" unless valid
|
|
91
|
+
|
|
92
|
+
value
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def integer(value, name)
|
|
96
|
+
raise Error, "#{name} must be an integer" unless value.is_a?(Integer)
|
|
97
|
+
|
|
98
|
+
value
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def uint(value, name, positive: false)
|
|
102
|
+
integer(value, name)
|
|
103
|
+
minimum = positive ? 1 : 0
|
|
104
|
+
raise Error, "#{name} must be at least #{minimum}" if value < minimum
|
|
105
|
+
|
|
106
|
+
value
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def boolean(value, name)
|
|
110
|
+
raise Error, "#{name} must be boolean" unless value == true || value == false
|
|
111
|
+
|
|
112
|
+
value
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def optional_string(value, name, **options)
|
|
116
|
+
value.nil? ? nil : string(value, name, **options)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def optional_uint(value, name)
|
|
120
|
+
value.nil? ? nil : uint(value, name)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def deep_freeze(value)
|
|
124
|
+
case value
|
|
125
|
+
when Hash
|
|
126
|
+
value.each { |key, item| deep_freeze(key); deep_freeze(item) }
|
|
127
|
+
when Array
|
|
128
|
+
value.each { |item| deep_freeze(item) }
|
|
129
|
+
end
|
|
130
|
+
value.freeze
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Megrez
|
|
4
|
+
module Results
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def capabilities(body)
|
|
8
|
+
Protocol.deep_freeze(Protocol.object(body, "initialize response").dup)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def breakpoints(body)
|
|
12
|
+
array(body, "breakpoints").map do |value|
|
|
13
|
+
value = Protocol.object(value, "breakpoint")
|
|
14
|
+
Breakpoint.new(
|
|
15
|
+
id: optional_integer(value["id"], "breakpoint id"),
|
|
16
|
+
verified: Protocol.boolean(value["verified"], "breakpoint verified"),
|
|
17
|
+
source: source(value["source"]),
|
|
18
|
+
line: optional_uint(value["line"], "breakpoint line"),
|
|
19
|
+
column: optional_uint(value["column"], "breakpoint column"),
|
|
20
|
+
message: Protocol.optional_string(value["message"], "breakpoint message")
|
|
21
|
+
)
|
|
22
|
+
end.freeze
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def threads(body)
|
|
26
|
+
array(body, "threads").map do |value|
|
|
27
|
+
value = Protocol.object(value, "thread")
|
|
28
|
+
ThreadInfo.new(
|
|
29
|
+
id: Protocol.integer(value["id"], "thread id"),
|
|
30
|
+
name: Protocol.string(value["name"], "thread name")
|
|
31
|
+
)
|
|
32
|
+
end.freeze
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def stack_frames(body)
|
|
36
|
+
array(body, "stackFrames").map do |value|
|
|
37
|
+
value = Protocol.object(value, "stack frame")
|
|
38
|
+
StackFrame.new(
|
|
39
|
+
id: Protocol.integer(value["id"], "stack frame id"),
|
|
40
|
+
name: Protocol.string(value["name"], "stack frame name"),
|
|
41
|
+
source: source(value["source"]),
|
|
42
|
+
line: Protocol.uint(value["line"], "stack frame line"),
|
|
43
|
+
column: Protocol.uint(value["column"], "stack frame column"),
|
|
44
|
+
presentation_hint: Protocol.optional_string(value["presentationHint"], "stack frame presentationHint")
|
|
45
|
+
)
|
|
46
|
+
end.freeze
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def scopes(body, generation)
|
|
50
|
+
array(body, "scopes").map do |value|
|
|
51
|
+
value = Protocol.object(value, "scope")
|
|
52
|
+
Scope.new(
|
|
53
|
+
name: Protocol.string(value["name"], "scope name"),
|
|
54
|
+
variables_reference: encode_reference(value["variablesReference"], generation),
|
|
55
|
+
expensive: Protocol.boolean(value["expensive"], "scope expensive"),
|
|
56
|
+
presentation_hint: Protocol.optional_string(value["presentationHint"], "scope presentationHint")
|
|
57
|
+
)
|
|
58
|
+
end.freeze
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def variables(body, generation)
|
|
62
|
+
array(body, "variables").map { |value| variable(value, generation) }.freeze
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def set_variable(body, generation, name)
|
|
66
|
+
value = Protocol.object(body, "setVariable response").merge("name" => name)
|
|
67
|
+
variable(value, generation)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def evaluate(body, generation, expression)
|
|
71
|
+
value = Protocol.object(body, "evaluate response").merge(
|
|
72
|
+
"name" => expression,
|
|
73
|
+
"value" => body["result"]
|
|
74
|
+
)
|
|
75
|
+
variable(value, generation)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def completions(body)
|
|
79
|
+
array(body, "targets").map do |value|
|
|
80
|
+
value = Protocol.object(value, "completion target")
|
|
81
|
+
Protocol.string(value["label"], "completion label")
|
|
82
|
+
%w[text sortText type].each do |key|
|
|
83
|
+
Protocol.optional_string(value[key], "completion #{key}") if value.key?(key)
|
|
84
|
+
end
|
|
85
|
+
%w[start length selectionStart selectionLength].each do |key|
|
|
86
|
+
Protocol.optional_uint(value[key], "completion #{key}") if value.key?(key)
|
|
87
|
+
end
|
|
88
|
+
Protocol.deep_freeze(value.dup)
|
|
89
|
+
end.freeze
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def source_content(body)
|
|
93
|
+
body = Protocol.object(body, "source response")
|
|
94
|
+
Protocol.string(body["content"], "source content")
|
|
95
|
+
Protocol.optional_string(body["mimeType"], "source mimeType") if body.key?("mimeType")
|
|
96
|
+
Protocol.deep_freeze(body.dup)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def body(body)
|
|
100
|
+
Protocol.deep_freeze(Protocol.object(body, "DAP response body").dup)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def encode_reference(value, generation)
|
|
104
|
+
value = Protocol.uint(value, "variablesReference")
|
|
105
|
+
return 0 if value.zero?
|
|
106
|
+
raise Error, "variablesReference is too large" if value > 0x7fff_ffff
|
|
107
|
+
|
|
108
|
+
(generation << 32) | value
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def decode_reference(value, generation)
|
|
112
|
+
value = Protocol.uint(value, "variables reference")
|
|
113
|
+
raise Error, "variables reference is not expandable" if value.zero?
|
|
114
|
+
|
|
115
|
+
encoded_generation = value >> 32
|
|
116
|
+
raise Error, "stale variables reference" unless encoded_generation == generation
|
|
117
|
+
|
|
118
|
+
value & 0xffff_ffff
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def variable(value, generation)
|
|
122
|
+
value = Protocol.object(value, "variable")
|
|
123
|
+
Variable.new(
|
|
124
|
+
name: Protocol.string(value["name"], "variable name"),
|
|
125
|
+
value: Protocol.string(value["value"], "variable value"),
|
|
126
|
+
type: Protocol.optional_string(value["type"], "variable type"),
|
|
127
|
+
variables_reference: encode_reference(value.fetch("variablesReference", 0), generation),
|
|
128
|
+
named_count: optional_uint(value["namedVariables"], "variable namedVariables"),
|
|
129
|
+
indexed_count: optional_uint(value["indexedVariables"], "variable indexedVariables"),
|
|
130
|
+
memory_reference: Protocol.optional_string(value["memoryReference"], "variable memoryReference")
|
|
131
|
+
)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def source(value)
|
|
135
|
+
return nil if value.nil?
|
|
136
|
+
|
|
137
|
+
value = Protocol.object(value, "source").dup
|
|
138
|
+
Protocol.optional_string(value["name"], "source name") if value.key?("name")
|
|
139
|
+
Protocol.optional_string(value["path"], "source path") if value.key?("path")
|
|
140
|
+
Protocol.optional_uint(value["sourceReference"], "source reference") if value.key?("sourceReference")
|
|
141
|
+
Protocol.deep_freeze(value)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def array(body, key)
|
|
145
|
+
body = Protocol.object(body, "DAP response body")
|
|
146
|
+
Protocol.collection(body[key], key)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def optional_uint(value, name) = value.nil? ? nil : Protocol.uint(value, name)
|
|
150
|
+
def optional_integer(value, name) = value.nil? ? nil : Protocol.integer(value, name)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Megrez
|
|
4
|
+
class Session
|
|
5
|
+
def threads
|
|
6
|
+
send_request("threads", {}, states: %i[configuring running stopped]) { |body| Results.threads(body) }
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def stack_trace(thread_id, start: 0, levels: 20)
|
|
10
|
+
thread_id = Protocol.integer(thread_id, "thread id")
|
|
11
|
+
start = Protocol.uint(start, "stack start")
|
|
12
|
+
levels = Protocol.uint(levels, "stack levels")
|
|
13
|
+
send_request(
|
|
14
|
+
"stackTrace",
|
|
15
|
+
{threadId: thread_id, startFrame: start, levels: levels},
|
|
16
|
+
states: [:stopped]
|
|
17
|
+
) { |body| Results.stack_frames(body) }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def scopes(frame_id)
|
|
21
|
+
frame_id = Protocol.integer(frame_id, "frame id")
|
|
22
|
+
stopped_generation = generation
|
|
23
|
+
send_request("scopes", {frameId: frame_id}, states: [:stopped]) do |body|
|
|
24
|
+
ensure_generation(stopped_generation)
|
|
25
|
+
Results.scopes(body, stopped_generation)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def variables(reference, start: nil, count: nil, filter: nil)
|
|
30
|
+
stopped_generation = generation
|
|
31
|
+
arguments = {variablesReference: Results.decode_reference(reference, stopped_generation)}
|
|
32
|
+
arguments[:start] = Protocol.uint(start, "variable start") unless start.nil?
|
|
33
|
+
arguments[:count] = Protocol.uint(count, "variable count") unless count.nil?
|
|
34
|
+
arguments[:filter] = variable_filter(filter) unless filter.nil?
|
|
35
|
+
send_request("variables", arguments, states: [:stopped]) do |body|
|
|
36
|
+
ensure_generation(stopped_generation)
|
|
37
|
+
Results.variables(body, stopped_generation)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def set_variable(reference, name, value)
|
|
42
|
+
stopped_generation = generation
|
|
43
|
+
reference = Results.decode_reference(reference, stopped_generation)
|
|
44
|
+
name = Protocol.string(name, "variable name")
|
|
45
|
+
value = Protocol.string(value, "variable value")
|
|
46
|
+
send_request(
|
|
47
|
+
"setVariable",
|
|
48
|
+
{variablesReference: reference, name: name, value: value},
|
|
49
|
+
states: [:stopped]
|
|
50
|
+
) do |body|
|
|
51
|
+
ensure_generation(stopped_generation)
|
|
52
|
+
Results.set_variable(body, stopped_generation, name)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def set_expression(expression, value, frame_id: nil)
|
|
57
|
+
stopped_generation = generation
|
|
58
|
+
expression = Protocol.string(expression, "expression", empty: false)
|
|
59
|
+
value = Protocol.string(value, "expression value")
|
|
60
|
+
arguments = {expression: expression, value: value}
|
|
61
|
+
arguments[:frameId] = Protocol.integer(frame_id, "frame id") unless frame_id.nil?
|
|
62
|
+
send_request("setExpression", arguments, states: [:stopped]) do |body|
|
|
63
|
+
ensure_generation(stopped_generation)
|
|
64
|
+
Results.set_variable(body, stopped_generation, expression)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def evaluate(expression, frame_id: nil, context: "repl")
|
|
69
|
+
stopped_generation = generation
|
|
70
|
+
expression = Protocol.string(expression, "expression")
|
|
71
|
+
context = Protocol.string(context, "evaluation context", empty: false)
|
|
72
|
+
arguments = {expression: expression, context: context}
|
|
73
|
+
arguments[:frameId] = Protocol.integer(frame_id, "frame id") unless frame_id.nil?
|
|
74
|
+
send_request("evaluate", arguments, states: [:stopped]) do |body|
|
|
75
|
+
ensure_generation(stopped_generation)
|
|
76
|
+
Results.evaluate(body, stopped_generation, expression)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def completions(text, column, frame_id: nil)
|
|
81
|
+
text = Protocol.string(text, "completion text")
|
|
82
|
+
column = Protocol.uint(column, "completion column")
|
|
83
|
+
arguments = {text: text, column: column}
|
|
84
|
+
arguments[:frameId] = Protocol.integer(frame_id, "frame id") unless frame_id.nil?
|
|
85
|
+
send_request("completions", arguments, states: [:stopped]) { |body| Results.completions(body) }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def source(reference)
|
|
89
|
+
reference = Protocol.uint(reference, "source reference")
|
|
90
|
+
send_request("source", {sourceReference: reference}, states: %i[configuring running stopped]) do |body|
|
|
91
|
+
Results.source_content(body)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def variable_filter(value)
|
|
98
|
+
value = value.to_s
|
|
99
|
+
raise ArgumentError, "filter must be indexed or named" unless %w[indexed named].include?(value)
|
|
100
|
+
|
|
101
|
+
value
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def ensure_generation(expected)
|
|
105
|
+
raise Error, "debuggee resumed while request was pending" unless generation == expected && state == :stopped
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|