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,79 +31,43 @@ 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-
67- # We set binmode for Windows compatibility
68- @stdin . binmode
69- @stdout . binmode
70- @stderr . binmode
71-
72- $stderr. puts ( "Ruby LSP Rails booting server" )
73- count = 0
37+ def rails_root
38+ T . must ( @rails_root )
39+ end
7440
75- begin
76- count += 1
77- initialize_response = T . must ( read_response )
78- @rails_root = T . let ( initialize_response [ :root ] , String )
79- rescue EmptyMessageError
80- $stderr. puts ( "Ruby LSP Rails is retrying initialize (#{ count } )" )
81- 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
8246 end
47+ end
48+ sig { override . params ( response : T ::Hash [ Symbol , T . untyped ] ) . void }
49+ def handle_initialize_response ( response )
50+ @rails_root = T . let ( response [ :root ] , T . nilable ( String ) )
51+ end
8352
84- $stderr . puts ( "Finished booting Ruby LSP Rails server" )
85-
53+ sig { override . void }
54+ def register_exit_handler
8655 unless ENV [ "RAILS_ENV" ] == "test"
8756 at_exit do
88- if @ wait_thread. alive?
89- $stderr . puts ( "Ruby LSP Rails is force killing the server")
57+ if wait_thread . alive?
58+ log_output ( " force killing the server")
9059 sleep ( 0.5 ) # give the server a bit of time if we already issued a shutdown notification
9160 force_kill
9261 end
9362 end
9463 end
95- rescue Errno ::EPIPE , IncompleteMessageError
96- raise InitializationError , @stderr . read
9764 end
9865
9966 sig { params ( name : String ) . returns ( T . nilable ( T ::Hash [ Symbol , T . untyped ] ) ) }
10067 def model ( name )
10168 make_request ( "model" , name : name )
10269 rescue IncompleteMessageError
103- $stderr . puts ( "Ruby LSP Rails failed to get model information: #{ @ stderr. read } ")
70+ log_output ( " failed to get model information: #{ stderr . read } ")
10471 nil
10572 end
10673
@@ -117,117 +84,47 @@ def association_target_location(model_name:, association_name:)
11784 association_name : association_name ,
11885 )
11986 rescue => e
120- $stderr. puts ( "Ruby LSP Rails failed with #{ e . message } : #{ @stderr . read } " )
87+ log_output ( "failed with #{ e . message } : #{ stderr . read } " )
88+ nil
12189 end
12290
12391 sig { params ( name : String ) . returns ( T . nilable ( T ::Hash [ Symbol , T . untyped ] ) ) }
12492 def route_location ( name )
12593 make_request ( "route_location" , name : name )
12694 rescue IncompleteMessageError
127- $stderr . puts ( "Ruby LSP Rails failed to get route location: #{ @ stderr. read } ")
95+ log_output ( " failed to get route location: #{ stderr . read } ")
12896 nil
12997 end
13098
13199 sig { params ( controller : String , action : String ) . returns ( T . nilable ( T ::Hash [ Symbol , T . untyped ] ) ) }
132100 def route ( controller :, action :)
133101 make_request ( "route_info" , controller : controller , action : action )
134102 rescue IncompleteMessageError
135- $stderr . puts ( "Ruby LSP Rails failed to get route information: #{ @ stderr. read } ")
103+ log_output ( " failed to get route information: #{ stderr . read } ")
136104 nil
137105 end
138106
139107 sig { void }
140108 def trigger_reload
141- $stderr . puts ( "Reloading Rails application ")
109+ log_output ( "triggering reload ")
142110 send_notification ( "reload" )
143111 rescue IncompleteMessageError
144- $stderr. puts ( "Ruby LSP Rails failed to trigger reload" )
145- nil
146- end
147-
148- sig { void }
149- def shutdown
150- $stderr. puts ( "Ruby LSP Rails shutting down server" )
151- send_message ( "shutdown" )
152- sleep ( 0.5 ) # give the server a bit of time to shutdown
153- [ @stdin , @stdout , @stderr ] . each ( &:close )
154- rescue IOError
155- # The server connection may have died
156- force_kill
157- end
158-
159- sig { returns ( T ::Boolean ) }
160- def stopped?
161- [ @stdin , @stdout , @stderr ] . all? ( &:closed? ) && !@wait_thread . alive?
162- end
163-
164- sig do
165- params (
166- request : String ,
167- params : T . nilable ( T ::Hash [ Symbol , T . untyped ] ) ,
168- ) . returns ( T . nilable ( T ::Hash [ Symbol , T . untyped ] ) )
169- end
170- def make_request ( request , params = nil )
171- send_message ( request , params )
172- read_response
173- end
174-
175- # Notifications are like messages, but one-way, with no response sent back.
176- sig { params ( request : String , params : T . nilable ( T ::Hash [ Symbol , T . untyped ] ) ) . void }
177- def send_notification ( request , params = nil ) = send_message ( request , params )
178-
179- private
180-
181- sig { overridable . params ( request : String , params : T . nilable ( T ::Hash [ Symbol , T . untyped ] ) ) . void }
182- def send_message ( request , params = nil )
183- message = { method : request }
184- message [ :params ] = params if params
185- json = message . to_json
186-
187- @mutex . synchronize do
188- @stdin . write ( "Content-Length: #{ json . length } \r \n \r \n " , json )
189- end
190- rescue Errno ::EPIPE
191- # The server connection died
192- end
193-
194- sig { overridable . returns ( T . nilable ( T ::Hash [ Symbol , T . untyped ] ) ) }
195- def read_response
196- raw_response = @mutex . synchronize do
197- headers = @stdout . gets ( "\r \n \r \n " )
198- raise IncompleteMessageError unless headers
199-
200- content_length = headers [ /Content-Length: (\d +)/i , 1 ] . to_i
201- raise EmptyMessageError if content_length . zero?
202-
203- @stdout . read ( content_length )
204- end
205-
206- response = JSON . parse ( T . must ( raw_response ) , symbolize_names : true )
207-
208- if response [ :error ]
209- $stderr. puts ( "Ruby LSP Rails error: " + response [ :error ] )
210- return
211- end
212-
213- response . fetch ( :result )
214- rescue Errno ::EPIPE
215- # The server connection died
112+ log_output ( "failed to trigger reload" )
216113 nil
217114 end
218-
219- sig { void }
220- def force_kill
221- # Windows does not support the `TERM` signal, so we're forced to use `KILL` here
222- Process . kill ( T . must ( Signal . list [ "KILL" ] ) , @wait_thread . pid )
223- end
224115 end
225116
226117 class NullClient < RunnerClient
227118 extend T ::Sig
228119
229120 sig { void }
230121 def initialize # rubocop:disable Lint/MissingSuper
122+ # no-op
123+ end
124+
125+ sig { override . params ( response : T ::Hash [ Symbol , T . untyped ] ) . void }
126+ def handle_initialize_response ( response )
127+ # no-op
231128 end
232129
233130 sig { override . void }
0 commit comments