rider-server 0.1.0

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