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,76 +31,44 @@ 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
- @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
71
40
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
79
46
end
47
+ end
80
48
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
82
53
54
+ sig { override . void }
55
+ def register_exit_handler
83
56
unless ENV [ "RAILS_ENV" ] == "test"
84
57
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")
87
60
sleep ( 0.5 ) # give the server a bit of time if we already issued a shutdown notification
88
61
force_kill
89
62
end
90
63
end
91
64
end
92
- rescue Errno ::EPIPE , IncompleteMessageError
93
- raise InitializationError , @stderr . read
94
65
end
95
66
96
67
sig { params ( name : String ) . returns ( T . nilable ( T ::Hash [ Symbol , T . untyped ] ) ) }
97
68
def model ( name )
98
69
make_request ( "model" , name : name )
99
70
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 } ")
101
72
nil
102
73
end
103
74
@@ -114,117 +85,47 @@ def association_target_location(model_name:, association_name:)
114
85
association_name : association_name ,
115
86
)
116
87
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
118
90
end
119
91
120
92
sig { params ( name : String ) . returns ( T . nilable ( T ::Hash [ Symbol , T . untyped ] ) ) }
121
93
def route_location ( name )
122
94
make_request ( "route_location" , name : name )
123
95
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 } ")
125
97
nil
126
98
end
127
99
128
100
sig { params ( controller : String , action : String ) . returns ( T . nilable ( T ::Hash [ Symbol , T . untyped ] ) ) }
129
101
def route ( controller :, action :)
130
102
make_request ( "route_info" , controller : controller , action : action )
131
103
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 } ")
133
105
nil
134
106
end
135
107
136
108
sig { void }
137
109
def trigger_reload
138
- $stderr . puts ( "Reloading Rails application ")
110
+ log_output ( "triggering reload ")
139
111
send_notification ( "reload" )
140
112
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" )
213
114
nil
214
115
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
221
116
end
222
117
223
118
class NullClient < RunnerClient
224
119
extend T ::Sig
225
120
226
121
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
228
129
end
229
130
230
131
sig { override . void }
0 commit comments