nrepl-lazuli 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/lib/nrepl/connection.rb +222 -0
- data/lib/nrepl/fake_stdout.rb +18 -0
- data/lib/nrepl/server.rb +77 -0
- data/lib/nrepl.rb +29 -0
- metadata +61 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 01bda1a2be06171508f3a03d476dd5d108939b703af0b039fd4e050a07622c1f
|
4
|
+
data.tar.gz: f85343da3cce42d114abbb1141705fa46aee8dcebe4b18198ef983a127cc2455
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 153895cd6a45830613cf7ebb0b7fa4fb61e45e8ee4d588e5d60b02669c12f5d3469a520e4fa80b0f9690785337e15d6db80ec2084c3330e8c609beb39ca268cb
|
7
|
+
data.tar.gz: b5a887a6b2fe214476828fbdd600d4d3be16846074e7e01e371d5a85f72253730e3e8ee3046effa7897a9e115f3a7d02cfa79e4917f5f04901caa963f87ae3c6
|
@@ -0,0 +1,222 @@
|
|
1
|
+
module NREPL
|
2
|
+
class Connection
|
3
|
+
@@debug_counter = 0
|
4
|
+
|
5
|
+
def initialize(input, debug: false, out: input, watches: NREPL.class_variable_get(:@@watches))
|
6
|
+
@debug = debug
|
7
|
+
@in = input
|
8
|
+
@out = out
|
9
|
+
@pending_evals = {}
|
10
|
+
@watches = watches
|
11
|
+
@counter = 0
|
12
|
+
end
|
13
|
+
|
14
|
+
def treat_messages!
|
15
|
+
bencode = BEncode::Parser.new(@in)
|
16
|
+
loop do
|
17
|
+
break if @in.eof?
|
18
|
+
msg = bencode.parse!
|
19
|
+
debug "Received", msg
|
20
|
+
next unless msg
|
21
|
+
treat_msg(msg)
|
22
|
+
end
|
23
|
+
@pending_evals.each { |(i, _)| clear_eval!(i) }
|
24
|
+
end
|
25
|
+
|
26
|
+
def treat_msg(msg)
|
27
|
+
case msg['op']
|
28
|
+
when 'clone'
|
29
|
+
register_session(msg)
|
30
|
+
when 'describe'
|
31
|
+
describe_msg(msg)
|
32
|
+
when 'eval'
|
33
|
+
eval_op(msg, false)
|
34
|
+
when 'eval_pause'
|
35
|
+
eval_op(msg, true)
|
36
|
+
when 'eval_resume'
|
37
|
+
msg['id'] ||= "eval_#{@counter += 1}"
|
38
|
+
stop_id = msg['stop_id']
|
39
|
+
clear_eval!(stop_id)
|
40
|
+
|
41
|
+
send_msg(response_for(msg, {
|
42
|
+
'status' => ['done'],
|
43
|
+
'op' => msg['op']
|
44
|
+
}))
|
45
|
+
when 'unwatch'
|
46
|
+
msg['id'] ||= "eval_#{@counter += 1}"
|
47
|
+
watch_id = msg['watch_id']
|
48
|
+
@watches.delete(watch_id)
|
49
|
+
|
50
|
+
send_msg(response_for(msg, {
|
51
|
+
'status' => ['done'],
|
52
|
+
'op' => msg['op']
|
53
|
+
}))
|
54
|
+
when 'interrupt'
|
55
|
+
id = if(msg['interrupt-id'])
|
56
|
+
msg['interrupt-id']
|
57
|
+
else
|
58
|
+
@pending_evals.keys.first
|
59
|
+
end
|
60
|
+
pending = @pending_evals[id] || {}
|
61
|
+
thread = pending[:thread]
|
62
|
+
msg['id'] ||= (id || 'unknown')
|
63
|
+
|
64
|
+
if(thread)
|
65
|
+
thread.kill
|
66
|
+
clear_eval!(id)
|
67
|
+
send_msg(response_for(msg, {
|
68
|
+
'status' => ['done', 'interrupted'],
|
69
|
+
'op' => msg['op']
|
70
|
+
}))
|
71
|
+
|
72
|
+
else
|
73
|
+
send_msg(response_for(msg, {
|
74
|
+
'status' => ['done'],
|
75
|
+
'op' => msg['op']
|
76
|
+
}))
|
77
|
+
end
|
78
|
+
|
79
|
+
else
|
80
|
+
send_msg(response_for(msg, {
|
81
|
+
'op' => msg['op'],
|
82
|
+
'status' => ['done', 'error'],
|
83
|
+
'error' => "unknown operation: #{msg['op'].inspect}"
|
84
|
+
}))
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
private def eval_op(msg, stop)
|
89
|
+
msg['id'] ||= "eval_#{@counter += 1}"
|
90
|
+
id = msg['id']
|
91
|
+
@pending_evals[id] = msg
|
92
|
+
@pending_evals[id][:thread] = Thread.new do
|
93
|
+
Thread.current[:eval_id] = msg['id']
|
94
|
+
|
95
|
+
begin
|
96
|
+
eval_msg(msg, stop)
|
97
|
+
rescue Exception => e
|
98
|
+
send_exception(msg, e)
|
99
|
+
ensure
|
100
|
+
@pending_evals.delete(id) unless stop
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
private def clear_eval!(id)
|
106
|
+
function_name = @pending_evals.fetch(id, {})[:stop_function_name]
|
107
|
+
if function_name
|
108
|
+
NREPL.singleton_class.send(:undef_method, "stop_#{function_name}")
|
109
|
+
end
|
110
|
+
|
111
|
+
input = @pending_evals.fetch(id, {})[:in]
|
112
|
+
input.close if input
|
113
|
+
|
114
|
+
@pending_evals.delete(id)
|
115
|
+
end
|
116
|
+
|
117
|
+
private def response_for(old_msg, msg)
|
118
|
+
msg.merge('session' => old_msg.fetch('session', 'none'), 'id' => old_msg.fetch('id', 'unknown'))
|
119
|
+
end
|
120
|
+
|
121
|
+
private def eval_msg(msg, stop)
|
122
|
+
str = msg['code']
|
123
|
+
code = str == 'nil' ? nil : str
|
124
|
+
pending_eval = @pending_evals[msg['id']]
|
125
|
+
value = unless code.nil?
|
126
|
+
if stop
|
127
|
+
@@debug_counter += 1
|
128
|
+
method_name = "_#{@@debug_counter}_#{rand(9999999999).to_s(32)}"
|
129
|
+
pending_eval[:stop_function_name] = method_name
|
130
|
+
code = code.gsub(/^NREPL\.stop!$/, "NREPL.stop_#{method_name}(binding)")
|
131
|
+
define_stop_function!(msg, method_name)
|
132
|
+
end
|
133
|
+
|
134
|
+
original_bind = if msg['stop_id']
|
135
|
+
@pending_evals.fetch(msg['stop_id'], {})[:binding]
|
136
|
+
elsif msg['watch_id']
|
137
|
+
@watches.fetch(msg['watch_id'], {})[:binding]
|
138
|
+
end
|
139
|
+
evaluate_code(code, msg['file'], msg['line'], original_bind)
|
140
|
+
end
|
141
|
+
|
142
|
+
unless pending_eval[:stopped?]
|
143
|
+
send_msg(response_for(msg, {'value' => value.to_s, 'status' => ['done']}))
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
private def define_stop_function!(msg, method_name)
|
148
|
+
out, inp = IO.pipe
|
149
|
+
send_stopped = proc do |ctx_binding, original_msg, caller|
|
150
|
+
stop_msg = @pending_evals[msg['id']]
|
151
|
+
if stop_msg
|
152
|
+
stop_msg.update(
|
153
|
+
in: inp,
|
154
|
+
binding: ctx_binding
|
155
|
+
)
|
156
|
+
end
|
157
|
+
@pending_evals[original_msg['id']].update(stopped?: true)
|
158
|
+
(file, row) = caller[0].split(/:/)
|
159
|
+
send_msg(response_for(original_msg, {
|
160
|
+
'file' => file,
|
161
|
+
'line' => row.to_i,
|
162
|
+
'status' => ['done', 'paused']
|
163
|
+
}))
|
164
|
+
end
|
165
|
+
|
166
|
+
will_pause = proc do
|
167
|
+
eval_id = while(thread = Thread.current)
|
168
|
+
break thread[:eval_id] if thread[:eval_id]
|
169
|
+
end
|
170
|
+
original_msg = @pending_evals[eval_id] || {}
|
171
|
+
stop_id = original_msg['stop_id']
|
172
|
+
original_msg if stop_id == msg['id']
|
173
|
+
end
|
174
|
+
NREPL.singleton_class.send(:define_method, "stop_#{method_name}") do |ctx_binding|
|
175
|
+
original_msg = will_pause.call
|
176
|
+
if original_msg
|
177
|
+
send_stopped.call(ctx_binding, original_msg, caller)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
private def register_session(msg)
|
183
|
+
debug "Register session", msg
|
184
|
+
|
185
|
+
id = rand(4294967087).to_s(16)
|
186
|
+
send_msg(response_for(msg, { 'new_session' => id, 'status' => ['done'] }))
|
187
|
+
end
|
188
|
+
|
189
|
+
private def debug(text, msg)
|
190
|
+
STDOUT.write("#{text}: #{msg.inspect}\n") if @debug
|
191
|
+
end
|
192
|
+
|
193
|
+
private def describe_msg(msg)
|
194
|
+
versions = {
|
195
|
+
ruby: RUBY_VERSION,
|
196
|
+
nrepl: NREPL::VERSION,
|
197
|
+
}
|
198
|
+
|
199
|
+
send_msg(response_for(msg, { 'versions' => versions }))
|
200
|
+
end
|
201
|
+
|
202
|
+
# @param [TCPSocket] client
|
203
|
+
# @param [Hash] msg
|
204
|
+
# @param [Exception] e
|
205
|
+
def send_exception(msg, e)
|
206
|
+
send_msg(response_for(msg, { 'ex' => e.message, 'status' => ['done', 'error'] }))
|
207
|
+
end
|
208
|
+
|
209
|
+
def send_msg(msg)
|
210
|
+
debug "Sending", msg
|
211
|
+
@out.write(msg.bencode)
|
212
|
+
@out.flush
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
# To avoid locally binding with the NREPL::Connection module
|
218
|
+
b = binding
|
219
|
+
define_method(:evaluate_code) do |code, file, line, bind|
|
220
|
+
bind ||= b
|
221
|
+
eval(code, bind, file || "EVAL", line || 0).inspect
|
222
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module NREPL
|
2
|
+
class FakeStdout
|
3
|
+
def initialize(connections, kind)
|
4
|
+
@connections = connections
|
5
|
+
@kind = kind
|
6
|
+
end
|
7
|
+
|
8
|
+
def write(text)
|
9
|
+
STDOUT.write "#{@connections}\n"
|
10
|
+
STDOUT.write(text)
|
11
|
+
@connections.each do |conn|
|
12
|
+
conn.send_msg(
|
13
|
+
@kind => text
|
14
|
+
)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/nrepl/server.rb
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
#
|
3
|
+
# A Ruby port of ogion https://gitlab.com/technomancy/ogion &
|
4
|
+
# https://github.com/borkdude/nrepl-server/blob/master/src/borkdude/nrepl_server.clj
|
5
|
+
|
6
|
+
require 'bencode'
|
7
|
+
require 'socket'
|
8
|
+
require_relative 'connection'
|
9
|
+
require_relative 'fake_stdout'
|
10
|
+
|
11
|
+
module NREPL
|
12
|
+
class Server
|
13
|
+
attr_reader :debug, :port, :host
|
14
|
+
alias debug? debug
|
15
|
+
|
16
|
+
def self.start(**kwargs)
|
17
|
+
new(**kwargs).start
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(port: DEFAULT_PORT, host: DEFAULT_HOST, debug: false)
|
21
|
+
@port = port
|
22
|
+
@host = host
|
23
|
+
@debug = debug
|
24
|
+
@connections = Set.new
|
25
|
+
NREPL.class_variable_set(:@@connections, @connections)
|
26
|
+
end
|
27
|
+
|
28
|
+
private def record_port
|
29
|
+
File.open(PORT_FILENAME, 'w+') do |f|
|
30
|
+
f.write(port)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def start
|
35
|
+
puts "nREPL server started on port #{port} on host #{host} - nrepl://#{host}:#{port}"
|
36
|
+
puts "Running in debug mode" if debug?
|
37
|
+
record_port
|
38
|
+
|
39
|
+
$stdout = FakeStdout.new(@connections, "out")
|
40
|
+
$stderr = FakeStdout.new(@connections, "err")
|
41
|
+
|
42
|
+
Signal.trap("INT") { stop }
|
43
|
+
Signal.trap("TERM") { stop }
|
44
|
+
|
45
|
+
s = TCPServer.new(host, port)
|
46
|
+
loop do
|
47
|
+
Thread.start(s.accept) do |client|
|
48
|
+
connection = Connection.new(client, debug: debug?)
|
49
|
+
@connections << connection
|
50
|
+
connection.treat_messages!
|
51
|
+
@connections.delete(connection)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
ensure
|
55
|
+
File.unlink(PORT_FILENAME)
|
56
|
+
end
|
57
|
+
|
58
|
+
def stop
|
59
|
+
Thread.exit
|
60
|
+
exit(0)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Sorry, no other way...
|
66
|
+
module ThreadPatch
|
67
|
+
def initialize(*args, &b)
|
68
|
+
@parent = Thread.current
|
69
|
+
super
|
70
|
+
end
|
71
|
+
|
72
|
+
def parent
|
73
|
+
@parent
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
Thread.prepend(ThreadPatch)
|
data/lib/nrepl.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NREPL
|
4
|
+
VERSION = '0.1.0'
|
5
|
+
DEFAULT_PORT = 7888
|
6
|
+
DEFAULT_HOST = '127.0.0.1'
|
7
|
+
PORT_FILENAME = '.nrepl-port'
|
8
|
+
|
9
|
+
require_relative 'nrepl/server'
|
10
|
+
@@watches = {}
|
11
|
+
@@connections = Set.new
|
12
|
+
|
13
|
+
def self.watch!(binding, id=nil)
|
14
|
+
(file, row) = caller[0].split(/:/)
|
15
|
+
id ||= "#{file}:#{row}"
|
16
|
+
row = row.to_i
|
17
|
+
|
18
|
+
@@watches[id] = {binding: binding}
|
19
|
+
@@connections.each do |connection|
|
20
|
+
connection.send_msg(
|
21
|
+
'op' => 'hit_watch',
|
22
|
+
'id' => id,
|
23
|
+
'file' => file,
|
24
|
+
'line' => row,
|
25
|
+
'status' => ['done']
|
26
|
+
)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
metadata
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: nrepl-lazuli
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Maurício Szabo
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-03-17 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bencode
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.8.2
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.8.2
|
27
|
+
description: A Ruby nREPL server, made to be used with Lazuli plug-in (but can be
|
28
|
+
used with any nREPL client too)
|
29
|
+
email: mauricio@szabo.link
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- lib/nrepl.rb
|
35
|
+
- lib/nrepl/connection.rb
|
36
|
+
- lib/nrepl/fake_stdout.rb
|
37
|
+
- lib/nrepl/server.rb
|
38
|
+
homepage: https://rubygems.org/gems/nrepl-lazuli
|
39
|
+
licenses:
|
40
|
+
- MIT
|
41
|
+
metadata: {}
|
42
|
+
post_install_message:
|
43
|
+
rdoc_options: []
|
44
|
+
require_paths:
|
45
|
+
- lib
|
46
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
47
|
+
requirements:
|
48
|
+
- - ">="
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: '0'
|
51
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
requirements: []
|
57
|
+
rubygems_version: 3.4.10
|
58
|
+
signing_key:
|
59
|
+
specification_version: 4
|
60
|
+
summary: A Ruby nREPL server
|
61
|
+
test_files: []
|