rider-server 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.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.build.yml +23 -0
  3. data/.ruby-version +1 -0
  4. data/.standard.yml +3 -0
  5. data/CHANGELOG.md +5 -0
  6. data/README.md +44 -0
  7. data/Rakefile +12 -0
  8. data/exe/rider-server +11 -0
  9. data/lib/rider_server/core_ext/array.rb +32 -0
  10. data/lib/rider_server/core_ext/hash.rb +14 -0
  11. data/lib/rider_server/core_ext/object.rb +18 -0
  12. data/lib/rider_server/core_ext/string.rb +18 -0
  13. data/lib/rider_server/core_ext/symbol.rb +14 -0
  14. data/lib/rider_server/errors.rb +16 -0
  15. data/lib/rider_server/exception_extension.rb +34 -0
  16. data/lib/rider_server/inspect.rb +148 -0
  17. data/lib/rider_server/logger.rb +13 -0
  18. data/lib/rider_server/operation.rb +69 -0
  19. data/lib/rider_server/operations.rb +136 -0
  20. data/lib/rider_server/ops/clone.rb +32 -0
  21. data/lib/rider_server/ops/close.rb +25 -0
  22. data/lib/rider_server/ops/completions.rb +100 -0
  23. data/lib/rider_server/ops/eval.rb +62 -0
  24. data/lib/rider_server/ops/inspect.rb +121 -0
  25. data/lib/rider_server/ops/inspect_exception.rb +47 -0
  26. data/lib/rider_server/ops/interrupt.rb +30 -0
  27. data/lib/rider_server/ops/load_path.rb +20 -0
  28. data/lib/rider_server/ops/lookup.rb +83 -0
  29. data/lib/rider_server/ops/ls_exceptions.rb +29 -0
  30. data/lib/rider_server/ops/ls_services.rb +19 -0
  31. data/lib/rider_server/ops/ls_sessions.rb +52 -0
  32. data/lib/rider_server/ops/service.rb +43 -0
  33. data/lib/rider_server/ops/set_namespace.rb +79 -0
  34. data/lib/rider_server/ops/set_namespace_variable.rb +80 -0
  35. data/lib/rider_server/ops/stdin.rb +20 -0
  36. data/lib/rider_server/ops/toggle_catch_all_exceptions.rb +27 -0
  37. data/lib/rider_server/response.rb +69 -0
  38. data/lib/rider_server/server.rb +104 -0
  39. data/lib/rider_server/service.rb +20 -0
  40. data/lib/rider_server/services/capture_exceptions.rb +62 -0
  41. data/lib/rider_server/services/capture_io.rb +302 -0
  42. data/lib/rider_server/services/rails.rb +129 -0
  43. data/lib/rider_server/session.rb +190 -0
  44. data/lib/rider_server/transports/bencode.rb +0 -0
  45. data/lib/rider_server/utils.rb +63 -0
  46. data/lib/rider_server/version.rb +12 -0
  47. data/lib/rider_server/workspace.rb +111 -0
  48. data/lib/rider_server.rb +5 -0
  49. metadata +122 -0
@@ -0,0 +1,302 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # capture_io.rb -- Handle IO
5
+ #
6
+ # Author: Russell Sim
7
+ # Copyright (c) 2024 Russell Sim
8
+ # SPDX-License-Identifier: MIT
9
+
10
+ require "rider_server/service"
11
+
12
+ module RiderServer
13
+ module Services
14
+ class CaptureIO < Service
15
+ attr_reader :stdin, :stdout, :stderr
16
+
17
+ @sessions = []
18
+ @stdin_stream, @stdin = ::IO.pipe
19
+
20
+ def self.stdin
21
+ @stdin
22
+ end
23
+
24
+ def self.sessions
25
+ @sessions
26
+ end
27
+
28
+ def self.nwrite(stream_name, *string)
29
+ str = string.join
30
+ send_response(stream_name, str)
31
+ end
32
+
33
+ def self.create_response(id, stream_name, str)
34
+ {
35
+ "id" => id,
36
+ stream_name => str,
37
+ "time-stamp" => Time.now.strftime("%Y-%m-%d %H:%M:%S")
38
+ }
39
+ end
40
+
41
+ def self.send_response(io_stream, string)
42
+ @sessions.each do |session, stream_id|
43
+ response = create_response(stream_id, io_stream, string)
44
+ session.response_queue.push(response)
45
+ end
46
+ end
47
+
48
+ def initialize(session)
49
+ @session = session
50
+ @stdout = ::STDOUT # rubocop:disable Style/GlobalStdStream
51
+ @stderr = ::STDERR # rubocop:disable Style/GlobalStdStream
52
+ super
53
+ end
54
+
55
+ def start(stream_id)
56
+ unless @stdin == ::STDIN && @stdout == ::STDOUT && @stderr == ::STDERR # rubocop:disable Style/GlobalStdStream
57
+ $stdin = self.class.stdin
58
+ $stdout = @stdout = Services::IO.new(::STDOUT, "out") # rubocop:disable Style/GlobalStdStream
59
+ $stderr = @stderr = Services::IO.new(::STDERR, "err") # rubocop:disable Style/GlobalStdStream
60
+ end
61
+ self.class.sessions << [@session, stream_id]
62
+ :running
63
+ end
64
+
65
+ def stop
66
+ self.class.sessions.delete_if { |s| s[0] == @session }
67
+
68
+ if self.class.sessions.empty?
69
+ $stdin = @stdin = ::STDIN # rubocop:disable Style/GlobalStdStream
70
+ $stdout = @stdout = ::STDOUT # rubocop:disable Style/GlobalStdStream
71
+ $stderr = @stderr = ::STDERR # rubocop:disable Style/GlobalStdStream
72
+ end
73
+ :stopped
74
+ end
75
+
76
+ def status
77
+ if self.class.sessions.find { |s| s[0] == @session }
78
+ :running
79
+ else
80
+ :stopped
81
+ end
82
+ end
83
+ end
84
+
85
+ class IO
86
+ def initialize(io, stream_name)
87
+ @stream_name = stream_name
88
+ @io = io
89
+ end
90
+
91
+ def write(*string)
92
+ strings = string.map(&:to_s)
93
+ Services::CaptureIO.nwrite(@stream_name, *strings)
94
+ @io.write(*strings)
95
+ end
96
+
97
+ def write_nonblock(string, **kwargs)
98
+ Services::CaptureIO.nwrite(@stream_name, string.to_s)
99
+ @io.write_nonblock(strings, **kwargs)
100
+ end
101
+
102
+ def syswrite(string)
103
+ Services::CaptureIO.nwrite(@stream_name, string.to_s)
104
+ @io.syswrite(string)
105
+ end
106
+
107
+ def <<(obj)
108
+ write(obj.to_s)
109
+ self
110
+ end
111
+
112
+ def print(*args)
113
+ if args.length == 0
114
+ write($_)
115
+ else
116
+ args.each_with_index do |arg, index|
117
+ write(arg.to_s)
118
+ if $, && index < args.length - 1
119
+ write($,)
120
+ end
121
+ end
122
+ nil
123
+ end
124
+ end
125
+
126
+ def printf(format_string, *args)
127
+ write(sprintf(format_string, *args))
128
+ nil
129
+ end
130
+
131
+ def putc(obj)
132
+ if obj.is_a?(String)
133
+ write(obj[0])
134
+ else
135
+ # The real version triggers a "TypeError: no implicit
136
+ # conversion of IO into Integer", when called on objects that
137
+ # are not numbers or strings and can't be coerced to a number.
138
+ write(obj.to_int.chr)
139
+ end
140
+ obj
141
+ end
142
+
143
+ def puts(*args)
144
+ args.each do |arg|
145
+ if arg.is_a?(Array)
146
+ arg.each do |a|
147
+ write(a.to_s, "\n")
148
+ end
149
+ else
150
+ write(arg.to_s, "\n")
151
+ end
152
+ end
153
+
154
+ write("\n") if args.length == 0
155
+ nil
156
+ end
157
+
158
+ def to_io
159
+ self
160
+ end
161
+
162
+ def pwrite(string, offset)
163
+ raise NotImplementedError
164
+ end
165
+
166
+ %i[
167
+ advise
168
+ all?
169
+ any?
170
+ autoclose?
171
+ bdecode
172
+ binmode
173
+ binmode?
174
+ chain
175
+ chunk
176
+ chunk_while
177
+ close
178
+ close_on_exec=
179
+ close_on_exec?
180
+ close_read
181
+ close_write
182
+ closed?
183
+ collect
184
+ collect_concat
185
+ compact
186
+ count
187
+ cycle
188
+ detect
189
+ drop
190
+ drop_while
191
+ each
192
+ each_byte
193
+ each_char
194
+ each_codepoint
195
+ each_cons
196
+ each_entry
197
+ each_line
198
+ each_slice
199
+ each_with_index
200
+ each_with_object
201
+ entries
202
+ eof
203
+ eof?
204
+ external_encoding
205
+ fcntl
206
+ fdatasync
207
+ fileno
208
+ filter
209
+ filter_map
210
+ find
211
+ find_all
212
+ find_index
213
+ first
214
+ flat_map
215
+ flush
216
+ fsync
217
+ getbyte
218
+ getc
219
+ gets
220
+ grep
221
+ grep_v
222
+ group_by
223
+ include?
224
+ inject
225
+ internal_encoding
226
+ ioctl
227
+ isatty
228
+ lazy
229
+ lineno
230
+ lineno=
231
+ map
232
+ max
233
+ max_by
234
+ member?
235
+ min
236
+ min_by
237
+ minmax
238
+ minmax_by
239
+ nonblock
240
+ nonblock=
241
+ nonblock?
242
+ none?
243
+ nread
244
+ one?
245
+ partition
246
+ pid
247
+ pos
248
+ pos=
249
+ pread
250
+ read
251
+ read_nonblock
252
+ readbyte
253
+ readchar
254
+ readline
255
+ readlines
256
+ readpartial
257
+ ready?
258
+ reduce
259
+ reject
260
+ reopen
261
+ reverse_each
262
+ rewind
263
+ seek
264
+ select
265
+ set_encoding
266
+ set_encoding_by_bom
267
+ slice_after
268
+ slice_before
269
+ slice_when
270
+ sort
271
+ sort_by
272
+ stat
273
+ sum
274
+ sync
275
+ sync=
276
+ sysread
277
+ sysseek
278
+ take
279
+ take_while
280
+ tally
281
+ tell
282
+ to_a
283
+ to_h
284
+ to_i
285
+ tty?
286
+ ungetbyte
287
+ ungetc
288
+ uniq
289
+ wait
290
+ wait_priority
291
+ wait_readable
292
+ wait_writable
293
+ zip
294
+ autoclose=
295
+ ].each do |method|
296
+ define_method(method) do |*args, **kwargs, &block|
297
+ @io.send(method, *args, **kwargs, &block)
298
+ end
299
+ end
300
+ end
301
+ end
302
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # rails.rb -- Rails service for Rider
5
+ #
6
+ # Author: Russell Sim
7
+ # Copyright (c) 2024 Russell Sim
8
+ # SPDX-License-Identifier: MIT
9
+
10
+ require "rider_server/service"
11
+
12
+ module RiderServer
13
+ module Services
14
+ class Rails < Service
15
+ def initialize(session)
16
+ @session = session
17
+ super
18
+ end
19
+
20
+ def start(stream_id)
21
+ @stream_id = stream_id
22
+
23
+ # Suck it ruby, i can define constants at runtime
24
+ eval('::APP_PATH = File.expand_path("./config/application", Dir.pwd)', TOPLEVEL_BINDING, __FILE__, __LINE__)
25
+ nputs "Starting Rails Server..."
26
+
27
+ @thread = Thread.new do
28
+ require File.expand_path("./config/boot", Dir.pwd)
29
+ require "rails/command"
30
+ require "rails/commands/server/server_command"
31
+
32
+ set_application_directory!
33
+
34
+ ::Rails::Server.new(server_options).tap do |server|
35
+ require APP_PATH
36
+
37
+ Dir.chdir(::Rails.application.root)
38
+
39
+ unless Rails.application.config.middleware.include?(Services::RailsExceptionMiddleware)
40
+ ::Rails.application.config.middleware.insert_after(
41
+ ActionDispatch::DebugExceptions, Services::RailsExceptionMiddleware, @stream_id, @session
42
+ )
43
+ end
44
+
45
+ if server.serveable?
46
+ nputs_boot_information(server.server, server.served_url)
47
+ after_stop_callback = -> { nputs "Rails Server Exiting..." }
48
+ server.start(after_stop_callback)
49
+ else
50
+ nputs rack_server_suggestion(options[:using])
51
+ end
52
+ end
53
+ rescue => e
54
+ @session.push_exception(stream_id, e)
55
+ nputs "Error starting Rails Server: #{e.message}"
56
+ end
57
+ end
58
+
59
+ def stop
60
+ @thread.raise(Interrupt)
61
+ end
62
+
63
+ def status
64
+ if @thread&.alive?
65
+ :running
66
+ else
67
+ :stopped
68
+ end
69
+ end
70
+
71
+ private
72
+
73
+ def set_application_directory!
74
+ Dir.chdir(File.expand_path("../..", APP_PATH)) unless File.exist?(File.expand_path("config.ru"))
75
+ end
76
+
77
+ def server_options
78
+ {
79
+ server: nil,
80
+ log_stdout: true,
81
+ Port: nil,
82
+ Host: nil,
83
+ DoNotReverseLookup: true,
84
+ config: "config.ru",
85
+ environment: "development",
86
+ daemonize: false,
87
+ pid: nil,
88
+ caching: nil,
89
+ restart_cmd: nil,
90
+ early_hints: nil
91
+ }
92
+ end
93
+
94
+ def nputs_boot_information(server, url)
95
+ nputs <<~MSG
96
+ => Booting #{ActiveSupport::Inflector.demodulize(server)}
97
+ => Rails #{::Rails.version} application starting in #{::Rails.env} #{url}
98
+ MSG
99
+ end
100
+
101
+ def nputs(string)
102
+ response = create_response
103
+ response["out"] = string + "\n"
104
+ @session.response_queue.push(response)
105
+ nil
106
+ end
107
+
108
+ def create_response
109
+ {
110
+ "id" => @stream_id,
111
+ "time-stamp" => Time.now.strftime("%Y-%m-%d %H:%M:%S")
112
+ }
113
+ end
114
+ end
115
+
116
+ class RailsExceptionMiddleware
117
+ def initialize(app)
118
+ @app = app
119
+ end
120
+
121
+ def call(env)
122
+ @app.call(env)
123
+ rescue => e
124
+ Services::CaptureExceptions.handle_exception(e, env)
125
+ raise
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # session.rb -- Represents a session in the Rider REPL
5
+ #
6
+ # Author: Russell Sim
7
+ # Copyright (c) 2024 Russell Sim
8
+ # SPDX-License-Identifier: MIT
9
+
10
+ require "date"
11
+ require "securerandom"
12
+ require "rider_server/workspace"
13
+ require "rider_server/errors"
14
+ require "rider_server/services/rails"
15
+ require "rider_server/services/capture_io"
16
+ require "rider_server/services/capture_exceptions"
17
+ require "rider_server/logger"
18
+
19
+ module RiderServer
20
+ class Session
21
+ include RiderServer::Logger
22
+
23
+ attr_reader :id
24
+ attr_reader :workspace
25
+ attr_reader :exceptions
26
+ attr_reader :evaluations
27
+ attr_reader :response_queue
28
+ attr_reader :history
29
+
30
+ SERVICES = [
31
+ Services::Rails,
32
+ Services::CaptureIO,
33
+ Services::CaptureExceptions
34
+ ]
35
+
36
+ def initialize(response_queue, history: [])
37
+ @id = SecureRandom.uuid
38
+ @history = history
39
+ @workspace = Workspace.new
40
+ @queue = Thread::Queue.new
41
+ @exceptions = Utils::FixedArray.new
42
+ @response_queue = response_queue
43
+ @exception_queue = Thread::Queue.new
44
+ @evaluations = {}
45
+ @services = SERVICES.each_with_object({}) do |klass, h|
46
+ h[klass.service_name] = klass.new(self)
47
+ end
48
+
49
+ # XXX Side effects in initializer, :()
50
+ start_exception_processing
51
+ end
52
+
53
+ def send_response(response)
54
+ if response
55
+ @response_queue.push(response)
56
+ end
57
+ end
58
+
59
+ def clone
60
+ Session.new(@responses_queue, history: @history.clone)
61
+ end
62
+
63
+ #
64
+ # Input History
65
+ #
66
+
67
+ def push_history(event)
68
+ @history.push(event)
69
+ end
70
+
71
+ def last_history
72
+ @history.last
73
+ end
74
+
75
+ #
76
+ # Historical Results
77
+ #
78
+
79
+ def result_history
80
+ @result_history ||= {}
81
+ end
82
+
83
+ def add_result(evaluation_id, value)
84
+ result_history[evaluation_id] = value
85
+ end
86
+
87
+ def get_result(evaluation_id)
88
+ raise "Missing history item #{evaluation_id}." unless result_history.key?(evaluation_id)
89
+ result_history[evaluation_id]
90
+ end
91
+
92
+ #
93
+ # Historical Exceptions
94
+ #
95
+
96
+ def add_exception(operation_id, exception, metadata = {})
97
+ id = SecureRandom.uuid
98
+ @exceptions << {
99
+ "id" => id,
100
+ "operation_id" => operation_id,
101
+ "created_at" => DateTime.now,
102
+ "exception" => exception,
103
+ "metadata" => metadata
104
+ }
105
+ id
106
+ end
107
+
108
+ def get_exception(exception_id)
109
+ exception = @exceptions.find { |item| item["id"] == exception_id }
110
+ raise "Missing exception #{exception_id}." unless exception
111
+ exception
112
+ end
113
+
114
+ # A threadsafe way to add exceptions
115
+ def push_exception(operation_id, exception, metadata = {})
116
+ @exception_queue.push([operation_id, exception, metadata])
117
+ end
118
+
119
+ def start_exception_processing
120
+ return if @exception_processing_thread
121
+
122
+ @exception_processing_thread = Thread.new do
123
+ loop do
124
+ add_exception(*@exception_queue.pop)
125
+ end
126
+ end
127
+ end
128
+
129
+ def stop_exception_processing
130
+ @exception_processing_thread.exit
131
+ @exception_processing_thread = nil
132
+ end
133
+
134
+ #
135
+ # Historical Evaluations
136
+ #
137
+
138
+ def add_evaluation(evaluation_id, eval_thread)
139
+ @evaluations[evaluation_id] = eval_thread
140
+ end
141
+
142
+ def remove_evaluation(evaluation_id)
143
+ @evaluations.delete(evaluation_id)
144
+ end
145
+
146
+ def running_evaluation?(evaluation_id)
147
+ @evaluations[evaluation_id]
148
+ end
149
+
150
+ def interrupt_evaluation(evaluation_id)
151
+ log.info "Interrupting eval thread #{evaluation_id}"
152
+ # Signal the eval thread to raise an exception
153
+ @evaluations[evaluation_id].raise EvalInterrupt.new
154
+ end
155
+
156
+ #
157
+ # Service control
158
+ #
159
+ def list_services
160
+ SERVICES.map do |klass|
161
+ {
162
+ "name" => klass.service_name,
163
+ "state" => service_state(klass.service_name).to_s
164
+ }
165
+ end
166
+ end
167
+
168
+ def start_service(service_type, stream_id)
169
+ raise "Service #{service_type} is already running." \
170
+ if @services[service_type].status == :running
171
+
172
+ @services[service_type].start(stream_id)
173
+ end
174
+
175
+ def stop_service(service_type)
176
+ raise "Service #{service_type} is not running." \
177
+ if @services[service_type].status == :stopped
178
+
179
+ @services[service_type].stop
180
+ end
181
+
182
+ def service_state(service_type)
183
+ if @services.key?(service_type)
184
+ @services[service_type].status
185
+ else
186
+ :stopped
187
+ end
188
+ end
189
+ end
190
+ end
File without changes
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # utils.rb -- Miscellaneous Utilities
5
+ #
6
+ # Author: Russell Sim
7
+ # Copyright (c) 2024 Russell Sim
8
+ # SPDX-License-Identifier: MIT
9
+
10
+ require "socket"
11
+ require "io/nonblock"
12
+
13
+ module RiderServer
14
+ module Utils
15
+ ##
16
+ # Creates TCP server sockets bound to +address+:+port+ and returns them.
17
+ #
18
+ # It will create IPV4 and IPV6 sockets on all interfaces.
19
+ def create_listeners(address, port)
20
+ unless port
21
+ raise ArgumentError, "must specify port"
22
+ end
23
+ sockets = Socket.tcp_server_sockets(address, port)
24
+ sockets.map { |s|
25
+ s.autoclose = false
26
+ ts = TCPServer.for_fd(s.fileno)
27
+ s.close
28
+ ts
29
+ }
30
+ end
31
+ module_function :create_listeners
32
+
33
+ def rider_inspect(obj)
34
+ if obj.respond_to?(:rider_inspect)
35
+ obj.rider_inspect
36
+ else
37
+ obj.inspect
38
+ end
39
+ end
40
+ module_function :rider_inspect
41
+
42
+ class FixedArray < Array
43
+ def initialize(*args)
44
+ @max_size = args[0] || 10
45
+ super
46
+ end
47
+
48
+ def <<(element)
49
+ super
50
+ shift while size > @max_size
51
+ end
52
+
53
+ alias_method :push, :<<
54
+ end
55
+ end
56
+
57
+ def create_response(operation)
58
+ {
59
+ "id" => operation["id"],
60
+ "time-stamp" => Time.now.strftime("%Y-%m-%d %H:%M:%S")
61
+ }
62
+ end
63
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # version.rb -- Rider version information
5
+ #
6
+ # Author: Russell Sim
7
+ # Copyright (c) 2024 Russell Sim
8
+ # SPDX-License-Identifier: MIT
9
+
10
+ module RiderServer
11
+ VERSION = "0.1.0"
12
+ end