3
3
4
4
require "json"
5
5
require "open3"
6
+ require "ruby_lsp/addon/process_client"
6
7
7
8
module RubyLsp
8
9
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
+
10
13
class << self
11
14
extend T ::Sig
12
15
13
- sig { returns ( RunnerClient ) }
14
- def create_client
16
+ sig { params ( addon : RubyLsp :: Addon ) . returns ( RunnerClient ) }
17
+ def create_client ( addon )
15
18
if File . exist? ( "bin/rails" )
16
- new
19
+ new ( addon , COMMAND )
17
20
else
18
21
$stderr. puts ( <<~MSG )
19
22
Ruby LSP Rails failed to locate bin/rails in the current directory: #{ Dir . pwd } "
@@ -28,79 +31,43 @@ def create_client
28
31
end
29
32
end
30
33
31
- class InitializationError < StandardError ; end
32
- class IncompleteMessageError < StandardError ; end
33
- class EmptyMessageError < StandardError ; end
34
-
35
- MAX_RETRIES = 5
36
-
37
34
extend T ::Sig
38
35
39
36
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
74
40
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
82
46
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
83
52
84
- $stderr . puts ( "Finished booting Ruby LSP Rails server" )
85
-
53
+ sig { override . void }
54
+ def register_exit_handler
86
55
unless ENV [ "RAILS_ENV" ] == "test"
87
56
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")
90
59
sleep ( 0.5 ) # give the server a bit of time if we already issued a shutdown notification
91
60
force_kill
92
61
end
93
62
end
94
63
end
95
- rescue Errno ::EPIPE , IncompleteMessageError
96
- raise InitializationError , @stderr . read
97
64
end
98
65
99
66
sig { params ( name : String ) . returns ( T . nilable ( T ::Hash [ Symbol , T . untyped ] ) ) }
100
67
def model ( name )
101
68
make_request ( "model" , name : name )
102
69
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 } ")
104
71
nil
105
72
end
106
73
@@ -117,117 +84,47 @@ def association_target_location(model_name:, association_name:)
117
84
association_name : association_name ,
118
85
)
119
86
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
121
89
end
122
90
123
91
sig { params ( name : String ) . returns ( T . nilable ( T ::Hash [ Symbol , T . untyped ] ) ) }
124
92
def route_location ( name )
125
93
make_request ( "route_location" , name : name )
126
94
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 } ")
128
96
nil
129
97
end
130
98
131
99
sig { params ( controller : String , action : String ) . returns ( T . nilable ( T ::Hash [ Symbol , T . untyped ] ) ) }
132
100
def route ( controller :, action :)
133
101
make_request ( "route_info" , controller : controller , action : action )
134
102
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 } ")
136
104
nil
137
105
end
138
106
139
107
sig { void }
140
108
def trigger_reload
141
- $stderr . puts ( "Reloading Rails application ")
109
+ log_output ( "triggering reload ")
142
110
send_notification ( "reload" )
143
111
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" )
216
113
nil
217
114
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
224
115
end
225
116
226
117
class NullClient < RunnerClient
227
118
extend T ::Sig
228
119
229
120
sig { void }
230
121
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
231
128
end
232
129
233
130
sig { override . void }
0 commit comments