nrepl-lazuli 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -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: []