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.
@@ -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"