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,79 @@
1
+ require "rider_server/operation"
2
+ require "rider_server/response"
3
+
4
+ module RiderServer
5
+ module Ops
6
+ class SetNamespace < Operation
7
+ documentation "Set the namespace to the given location."
8
+
9
+ argument :id, :string, "The request id", required: true
10
+ argument :location, :array, "The location to set the namespace to", required: true
11
+
12
+ def handle(session, operation)
13
+ response = Response.new(operation)
14
+ location = operation["location"]
15
+
16
+ value = traverse_location(location, nil, session)
17
+ session.workspace.set_namespace(value)
18
+ response.set("ns", session.workspace.namespace_name)
19
+ response.set("location", location)
20
+ response.status("done")
21
+ response
22
+ end
23
+
24
+ def traverse_location(location, ctx, session)
25
+ loc = location.first
26
+ locs = location.drop(1)
27
+
28
+ if locs.empty?
29
+ lookup_object(loc, ctx, session)
30
+ else
31
+ traverse_location(locs, lookup_object(loc, ctx, session), session)
32
+ end
33
+ end
34
+
35
+ def lookup_object(loc, ctx, session)
36
+ lookup, identifier = loc.split(":", 2)
37
+
38
+ case lookup
39
+ when "rider_main"
40
+ "main"
41
+ when "rider_history"
42
+ rider_history(identifier, nil, session)
43
+ when "rider_exception"
44
+ rider_exception(identifier, nil, session)
45
+ when "rider_stackframe"
46
+ ctx.__rider_bindings_stack[identifier.to_i]
47
+ when "rider_stackframe_variable"
48
+ frame_id, local_var = identifier.split(":", 2)
49
+ ctx.__rider_bindings_stack[frame_id.to_i].local_variable_get(local_var)
50
+ when "rider_hash_key"
51
+ hash = identifier.to_i
52
+ ctx.keys.find { |key| key.hash == hash }
53
+ when "rider_hash_value"
54
+ hash = identifier.to_i
55
+ key = ctx.keys.find { |key| key.hash == hash }
56
+ ctx.fetch(key)
57
+ when "toplevel_const_get"
58
+ Object.const_get(identifier)
59
+ when "instance_variable_get"
60
+ ctx.instance_variable_get(identifier)
61
+ when "const_get"
62
+ ctx.const_get(identifier)
63
+ when "ancestor_find"
64
+ ctx.class.ancestors.find { |item| item.to_s == identifier }
65
+ else
66
+ raise "Unknown inspect function: #{lookup}"
67
+ end
68
+ end
69
+
70
+ def rider_history(value, ctx, session)
71
+ session.get_result(value)
72
+ end
73
+
74
+ def rider_exception(id, ctx, session)
75
+ session.get_exception(id)["exception"]
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,80 @@
1
+ require "rider_server/operation"
2
+ require "rider_server/response"
3
+
4
+ module RiderServer
5
+ module Ops
6
+ class SetNamespaceVariable < Operation
7
+ documentation "Set a variable in the current REPL binding."
8
+
9
+ argument :id, :string, "The request id", required: true
10
+ argument :name, :string, "The name of the local variable to set", required: true
11
+ argument :location, :array, "The location to set the namespace to", required: true
12
+
13
+ def handle(session, operation)
14
+ response = Response.new(operation)
15
+ name = operation["name"]
16
+ raise "Name must be a valid Ruby identifier" unless name =~ /\A[a-zA-Z_]\w*\z/
17
+ location = operation["location"]
18
+
19
+ value = traverse_location(location, nil, session)
20
+ session.workspace.binding_local_variable_set(name, value)
21
+ response.status("done")
22
+ response
23
+ end
24
+
25
+ def traverse_location(location, ctx, session)
26
+ loc = location.first
27
+ locs = location.drop(1)
28
+
29
+ if locs.empty?
30
+ lookup_object(loc, ctx, session)
31
+ else
32
+ traverse_location(locs, lookup_object(loc, ctx, session), session)
33
+ end
34
+ end
35
+
36
+ def lookup_object(loc, ctx, session)
37
+ lookup, identifier = loc.split(":", 2)
38
+
39
+ case lookup
40
+ when "rider_main"
41
+ "main"
42
+ when "rider_history"
43
+ rider_history(identifier, nil, session)
44
+ when "rider_exception"
45
+ rider_exception(identifier, nil, session)
46
+ when "rider_stackframe"
47
+ ctx.__rider_bindings_stack[identifier.to_i]
48
+ when "rider_stackframe_variable"
49
+ frame_id, local_var = identifier.split(":", 2)
50
+ ctx.__rider_bindings_stack[frame_id.to_i].local_variable_get(local_var)
51
+ when "rider_hash_key"
52
+ hash = identifier.to_i
53
+ ctx.keys.find { |key| key.hash == hash }
54
+ when "rider_hash_value"
55
+ hash = identifier.to_i
56
+ key = ctx.keys.find { |key| key.hash == hash }
57
+ ctx.fetch(key)
58
+ when "toplevel_const_get"
59
+ Object.const_get(identifier)
60
+ when "instance_variable_get"
61
+ ctx.instance_variable_get(identifier)
62
+ when "const_get"
63
+ ctx.const_get(identifier)
64
+ when "ancestor_find"
65
+ ctx.class.ancestors.find { |item| item.to_s == identifier }
66
+ else
67
+ raise "Unknown inspect function: #{lookup}"
68
+ end
69
+ end
70
+
71
+ def rider_history(value, ctx, session)
72
+ session.get_result(value)
73
+ end
74
+
75
+ def rider_exception(id, ctx, session)
76
+ session.get_exception(id)["exception"]
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,20 @@
1
+ require "rider_server/operation"
2
+ require "rider_server/response"
3
+
4
+ module RiderServer
5
+ module Ops
6
+ class Stdin < Operation
7
+ documentation "List all exceptions that have occurred in the session."
8
+
9
+ argument :id, :string, "The request id", required: true
10
+ argument :stdin, :string, "The input to write to the stdin of the process", required: true
11
+
12
+ def handle(session, operation)
13
+ controller.stdin.write(operation["stdin"])
14
+ response = Response.new(operation)
15
+ response.status("done")
16
+ response
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,27 @@
1
+ require "rider_server/operation"
2
+ require "rider_server/response"
3
+
4
+ module RiderServer
5
+ module Ops
6
+ class ToggleCatchAllExceptions < Operation
7
+ documentation "Enable catching all exceptions. Not just the ones with a session."
8
+
9
+ argument :id, :string, "The request id", required: true
10
+
11
+ def handle(session, operation)
12
+ if @controller.sessions_catching_exceptions.member?(session.id)
13
+ @controller.sessions_catching_exceptions.delete(session.id)
14
+ value = "disabled"
15
+ else
16
+ @controller.sessions_catching_exceptions.push(session.id)
17
+ value = "enabled"
18
+ end
19
+
20
+ response = Response.new(operation)
21
+ response.status("done")
22
+ response.set("value", value)
23
+ response
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # operations.rb -- Handle all the nRepl op codes
5
+ #
6
+ # Author: Russell Sim
7
+ # Copyright (c) 2024 Russell Sim
8
+ # SPDX-License-Identifier: MIT
9
+
10
+ require "bencode"
11
+
12
+ module RiderServer
13
+ class Response
14
+ def initialize(operation)
15
+ raise "Operation ID cannot be nil" if operation["id"].nil?
16
+ @op = operation["op"]
17
+
18
+ @data = {
19
+ "id" => operation["id"],
20
+ "timestamp" => Time.now.strftime("%Y-%m-%d %H:%M:%S"),
21
+ "status" => []
22
+ }
23
+
24
+ if operation["session"]
25
+ @data["session"] = operation["session"]
26
+ end
27
+ end
28
+
29
+ def id
30
+ @data["id"]
31
+ end
32
+
33
+ def session
34
+ @data["session"]
35
+ end
36
+
37
+ def set(key, value)
38
+ @data[key] = value
39
+ end
40
+
41
+ def fetch(*args)
42
+ @data.fetch(*args)
43
+ end
44
+
45
+ def [](item)
46
+ @data[item]
47
+ end
48
+
49
+ def status(*value)
50
+ @data["status"].concat(value)
51
+ end
52
+
53
+ def done?
54
+ @data["status"].include?("done")
55
+ end
56
+
57
+ def bencode
58
+ @data.bencode
59
+ end
60
+
61
+ def inspect
62
+ "#<RiderServer::Response:#{object_id} id=#{id} session=#{session} op=#{@op} status=#{@data["status"]}>"
63
+ end
64
+
65
+ def to_h
66
+ @data
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # server.rb -- Server Class
5
+ #
6
+ # Author: Russell Sim
7
+ # Copyright (c) 2024 Russell Sim
8
+ # SPDX-License-Identifier: MIT
9
+
10
+ require "rider_server/exception_extension"
11
+ require "rider_server/operations"
12
+ require "rider_server/utils"
13
+ require "rider_server/logger"
14
+ require "bencode"
15
+
16
+ module RiderServer
17
+ class Server
18
+ include RiderServer::Logger
19
+
20
+ attr_reader :status
21
+ attr_reader :listeners
22
+
23
+ def initialize
24
+ @status = :stopped
25
+ @shutdown_pipe = nil
26
+ end
27
+
28
+ def listen(address, port)
29
+ Utils.create_listeners(address, port)
30
+ end
31
+
32
+ def setup_shutdown_pipe
33
+ @shutdown_pipe ||= IO.pipe
34
+ end
35
+
36
+ def run
37
+ socket = listen("127.0.0.1", 7888)
38
+ request_count = 0
39
+
40
+ responses = Thread::Queue.new
41
+ operations = Operations.new(responses)
42
+
43
+ loop do
44
+ client = socket[0].accept
45
+
46
+ request_count += 1
47
+
48
+ threads = []
49
+ threads << Thread.new {
50
+ log.debug "Starting response thread"
51
+ loop do
52
+ response = responses.pop
53
+
54
+ if response == :shutdown
55
+ break
56
+ end
57
+
58
+ begin
59
+ output = response.bencode
60
+ rescue StandardError => e
61
+ log.error "Error encoding response: #{e}"
62
+ response = Response.new(response.to_h)
63
+ response.set("ex", e.inspect)
64
+ response.set("out", e.full_message)
65
+ response.status("eval-error")
66
+ output = response.bencode
67
+ end
68
+
69
+ if log.debug?
70
+ log.debug "sending response #{response.to_h}"
71
+ else
72
+ log.info "sending response #{response.inspect}"
73
+ end
74
+ client.write(output)
75
+ # Try to allow time for the client to read the response
76
+ sleep 0.01
77
+ end
78
+ }
79
+
80
+ threads << Thread.new {
81
+ parser = BEncode::Parser.new(client)
82
+ loop do
83
+ # Peek at the next character, and exit if it is nil
84
+ c = client.getc
85
+ if c.nil?
86
+ responses.push :shutdown
87
+ log.debug("Connection closed, closing")
88
+ break
89
+ end
90
+ client.ungetc(c)
91
+
92
+ operation = parser.parse!
93
+
94
+ log.debug(operation.inspect)
95
+ operations.handle(operation)
96
+ end
97
+ }
98
+ threads.each { |thr| thr.join }
99
+
100
+ client.close
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # service.rb -- Base class for services
5
+ #
6
+ # Author: Russell Sim
7
+ # Copyright (c) 2024 Russell Sim
8
+ # SPDX-License-Identifier: MIT
9
+
10
+ module RiderServer
11
+ class Service
12
+ def self.service_name
13
+ name.split("::").last.gsub(/(.)([A-Z])/, '\1_\2').downcase
14
+ end
15
+
16
+ def initialize(session)
17
+ @session = session
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # capture_exception.rb -- Capture exceptions and send them to the session
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 CaptureExceptions < Service
15
+ @sessions = []
16
+
17
+ def self.sessions
18
+ @sessions
19
+ end
20
+
21
+ # Take an exception object created by the session and create a
22
+ # response to send back to the client
23
+ def self.create_response(id, exception)
24
+ {
25
+ "id" => id,
26
+ "rider/exception-id" => exception.id,
27
+ "time-stamp" => Time.now.strftime("%Y-%m-%d %H:%M:%S")
28
+ }
29
+ end
30
+
31
+ def self.handle_exception(exception, metadata = {})
32
+ @sessions.each do |session, stream_id|
33
+ ex = session.add_exception(stream_id, exception, metadata)
34
+ session.response_queue.push(create_response(stream_id, ex))
35
+ end
36
+ end
37
+
38
+ def initialize(session)
39
+ @session = session
40
+ end
41
+
42
+ def start(stream_id)
43
+ @stream_id = stream_id
44
+ self.class.sessions.push([@session, @stream_id])
45
+ :running
46
+ end
47
+
48
+ def stop
49
+ self.class.sessions.delete_if { |s| s[0] == @session }
50
+ :stopped
51
+ end
52
+
53
+ def status
54
+ if self.class.sessions.find { |s| s[0] == @session }
55
+ :running
56
+ else
57
+ :stopped
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end