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 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: []