33
44require "json"
55require "open3"
6+ require "ruby_lsp/addon/process_client"
67
78module RubyLsp
89 module Rails
9- class RunnerClient
10+ class RunnerClient < RubyLsp ::Addon ::ProcessClient
11+ COMMAND = T . let ( [ "bundle" , "exec" , "rails" , "runner" , "#{ __dir__ } /server.rb" , "start" ] . join ( " " ) , String )
12+
1013 class << self
1114 extend T ::Sig
1215
13- sig { returns ( RunnerClient ) }
14- def create_client
16+ sig { params ( addon : RubyLsp :: Addon ) . returns ( RunnerClient ) }
17+ def create_client ( addon )
1518 if File . exist? ( "bin/rails" )
16- new
19+ new ( addon , COMMAND )
1720 else
1821 $stderr. puts ( <<~MSG )
1922 Ruby LSP Rails failed to locate bin/rails in the current directory: #{ Dir . pwd } "
@@ -28,76 +31,44 @@ def create_client
2831 end
2932 end
3033
31- class InitializationError < StandardError ; end
32- class IncompleteMessageError < StandardError ; end
33- class EmptyMessageError < StandardError ; end
34-
35- MAX_RETRIES = 5
36-
3734 extend T ::Sig
3835
3936 sig { returns ( String ) }
40- attr_reader :rails_root
41-
42- sig { void }
43- def initialize
44- @mutex = T . let ( Mutex . new , Mutex )
45- # Spring needs a Process session ID. It uses this ID to "attach" itself to the parent process, so that when the
46- # parent ends, the spring process ends as well. If this is not set, Spring will throw an error while trying to
47- # set its own session ID
48- begin
49- Process . setpgrp
50- Process . setsid
51- rescue Errno ::EPERM
52- # If we can't set the session ID, continue
53- rescue NotImplementedError
54- # setpgrp() may be unimplemented on some platform
55- # https://github.com/Shopify/ruby-lsp-rails/issues/348
56- end
57-
58- stdin , stdout , stderr , wait_thread = Bundler . with_original_env do
59- Open3 . popen3 ( "bundle" , "exec" , "rails" , "runner" , "#{ __dir__ } /server.rb" , "start" )
60- end
61-
62- @stdin = T . let ( stdin , IO )
63- @stdout = T . let ( stdout , IO )
64- @stderr = T . let ( stderr , IO )
65- @wait_thread = T . let ( wait_thread , Process ::Waiter )
66- @stdin . binmode # for Windows compatibility
67- @stdout . binmode # for Windows compatibility
68-
69- $stderr. puts ( "Ruby LSP Rails booting server" )
70- count = 0
37+ def rails_root
38+ T . must ( @rails_root )
39+ end
7140
72- begin
73- count += 1
74- initialize_response = T . must ( read_response )
75- @rails_root = T . let ( initialize_response [ :root ] , String )
76- rescue EmptyMessageError
77- $stderr. puts ( "Ruby LSP Rails is retrying initialize (#{ count } )" )
78- retry if count < MAX_RETRIES
41+ sig { params ( message : String ) . void }
42+ def log_output ( message )
43+ # We don't want to log output in tests
44+ unless ENV [ "RAILS_ENV" ] == "test"
45+ super
7946 end
47+ end
8048
81- $stderr. puts ( "Finished booting Ruby LSP Rails server" )
49+ sig { override . params ( response : T ::Hash [ Symbol , T . untyped ] ) . void }
50+ def handle_initialize_response ( response )
51+ @rails_root = T . let ( response [ :root ] , T . nilable ( String ) )
52+ end
8253
54+ sig { override . void }
55+ def register_exit_handler
8356 unless ENV [ "RAILS_ENV" ] == "test"
8457 at_exit do
85- if @ wait_thread. alive?
86- $stderr . puts ( "Ruby LSP Rails is force killing the server")
58+ if wait_thread . alive?
59+ log_output ( " force killing the server")
8760 sleep ( 0.5 ) # give the server a bit of time if we already issued a shutdown notification
8861 force_kill
8962 end
9063 end
9164 end
92- rescue Errno ::EPIPE , IncompleteMessageError
93- raise InitializationError , @stderr . read
9465 end
9566
9667 sig { params ( name : String ) . returns ( T . nilable ( T ::Hash [ Symbol , T . untyped ] ) ) }
9768 def model ( name )
9869 make_request ( "model" , name : name )
9970 rescue IncompleteMessageError
100- $stderr . puts ( "Ruby LSP Rails failed to get model information: #{ @ stderr. read } ")
71+ log_output ( " failed to get model information: #{ stderr . read } ")
10172 nil
10273 end
10374
@@ -114,117 +85,47 @@ def association_target_location(model_name:, association_name:)
11485 association_name : association_name ,
11586 )
11687 rescue => e
117- $stderr. puts ( "Ruby LSP Rails failed with #{ e . message } : #{ @stderr . read } " )
88+ log_output ( "failed with #{ e . message } : #{ stderr . read } " )
89+ nil
11890 end
11991
12092 sig { params ( name : String ) . returns ( T . nilable ( T ::Hash [ Symbol , T . untyped ] ) ) }
12193 def route_location ( name )
12294 make_request ( "route_location" , name : name )
12395 rescue IncompleteMessageError
124- $stderr . puts ( "Ruby LSP Rails failed to get route location: #{ @ stderr. read } ")
96+ log_output ( " failed to get route location: #{ stderr . read } ")
12597 nil
12698 end
12799
128100 sig { params ( controller : String , action : String ) . returns ( T . nilable ( T ::Hash [ Symbol , T . untyped ] ) ) }
129101 def route ( controller :, action :)
130102 make_request ( "route_info" , controller : controller , action : action )
131103 rescue IncompleteMessageError
132- $stderr . puts ( "Ruby LSP Rails failed to get route information: #{ @ stderr. read } ")
104+ log_output ( " failed to get route information: #{ stderr . read } ")
133105 nil
134106 end
135107
136108 sig { void }
137109 def trigger_reload
138- $stderr . puts ( "Reloading Rails application ")
110+ log_output ( "triggering reload ")
139111 send_notification ( "reload" )
140112 rescue IncompleteMessageError
141- $stderr. puts ( "Ruby LSP Rails failed to trigger reload" )
142- nil
143- end
144-
145- sig { void }
146- def shutdown
147- $stderr. puts ( "Ruby LSP Rails shutting down server" )
148- send_message ( "shutdown" )
149- sleep ( 0.5 ) # give the server a bit of time to shutdown
150- [ @stdin , @stdout , @stderr ] . each ( &:close )
151- rescue IOError
152- # The server connection may have died
153- force_kill
154- end
155-
156- sig { returns ( T ::Boolean ) }
157- def stopped?
158- [ @stdin , @stdout , @stderr ] . all? ( &:closed? ) && !@wait_thread . alive?
159- end
160-
161- private
162-
163- sig do
164- params (
165- request : String ,
166- params : T . nilable ( T ::Hash [ Symbol , T . untyped ] ) ,
167- ) . returns ( T . nilable ( T ::Hash [ Symbol , T . untyped ] ) )
168- end
169- def make_request ( request , params = nil )
170- send_message ( request , params )
171- read_response
172- end
173-
174- sig { overridable . params ( request : String , params : T . nilable ( T ::Hash [ Symbol , T . untyped ] ) ) . void }
175- def send_message ( request , params = nil )
176- message = { method : request }
177- message [ :params ] = params if params
178- json = message . to_json
179-
180- @mutex . synchronize do
181- @stdin . write ( "Content-Length: #{ json . length } \r \n \r \n " , json )
182- end
183- rescue Errno ::EPIPE
184- # The server connection died
185- end
186-
187- # Notifications are like messages, but one-way, with no response sent back.
188- sig { params ( request : String , params : T . nilable ( T ::Hash [ Symbol , T . untyped ] ) ) . void }
189- def send_notification ( request , params = nil ) = send_message ( request , params )
190-
191- sig { overridable . returns ( T . nilable ( T ::Hash [ Symbol , T . untyped ] ) ) }
192- def read_response
193- raw_response = @mutex . synchronize do
194- headers = @stdout . gets ( "\r \n \r \n " )
195- raise IncompleteMessageError unless headers
196-
197- content_length = headers [ /Content-Length: (\d +)/i , 1 ] . to_i
198- raise EmptyMessageError if content_length . zero?
199-
200- @stdout . read ( content_length )
201- end
202-
203- response = JSON . parse ( T . must ( raw_response ) , symbolize_names : true )
204-
205- if response [ :error ]
206- $stderr. puts ( "Ruby LSP Rails error: " + response [ :error ] )
207- return
208- end
209-
210- response . fetch ( :result )
211- rescue Errno ::EPIPE
212- # The server connection died
113+ log_output ( "failed to trigger reload" )
213114 nil
214115 end
215-
216- sig { void }
217- def force_kill
218- # Windows does not support the `TERM` signal, so we're forced to use `KILL` here
219- Process . kill ( T . must ( Signal . list [ "KILL" ] ) , @wait_thread . pid )
220- end
221116 end
222117
223118 class NullClient < RunnerClient
224119 extend T ::Sig
225120
226121 sig { void }
227- def initialize # rubocop:disable Lint/MissingSuper
122+ def initialize
123+ # no-op
124+ end
125+
126+ sig { override . params ( response : T ::Hash [ Symbol , T . untyped ] ) . void }
127+ def handle_initialize_response ( response )
128+ # no-op
228129 end
229130
230131 sig { override . void }
0 commit comments