@@ -19,7 +19,43 @@ def send_message(message)
19
19
# Log a message to the editor's output panel
20
20
def log_message ( message )
21
21
$stderr. puts ( message )
22
- send_message ( { result : nil } )
22
+ end
23
+
24
+ # Sends an error result to a request, if the request failed. DO NOT INVOKE THIS METHOD FOR NOTIFICATIONS! Use
25
+ # `log_message` instead, otherwise the client/server communication will go out of sync
26
+ def send_error_response ( message )
27
+ send_message ( { error : message } )
28
+ end
29
+
30
+ # Sends a result back to the client
31
+ def send_result ( result )
32
+ send_message ( { result : result } )
33
+ end
34
+
35
+ # Handle possible errors for a request. This should only be used for requests, which means messages that return a
36
+ # response back to the client. Errors are returned as an error object back to the client
37
+ def with_request_error_handling ( request_name , &block )
38
+ block . call
39
+ rescue ActiveRecord ::ConnectionNotEstablished
40
+ # Since this is a common problem, we show a specific error message to the user, instead of the full stack trace.
41
+ send_error_response ( "Request #{ request_name } failed because database connection was not established." )
42
+ rescue ActiveRecord ::NoDatabaseError
43
+ send_error_response ( "Request #{ request_name } failed because the database does not exist." )
44
+ rescue => e
45
+ send_error_response ( "Request #{ request_name } failed:\n #{ e . full_message ( highlight : false ) } " )
46
+ end
47
+
48
+ # Handle possible errors for a notification. This should only be used for notifications, which means messages that
49
+ # do not return a response back to the client. Errors are logged to the editor's output panel
50
+ def with_notification_error_handling ( notification_name , &block )
51
+ block . call
52
+ rescue ActiveRecord ::ConnectionNotEstablished
53
+ # Since this is a common problem, we show a specific error message to the user, instead of the full stack trace.
54
+ log_message ( "Request #{ notification_name } failed because database connection was not established." )
55
+ rescue ActiveRecord ::NoDatabaseError
56
+ log_message ( "Request #{ notification_name } failed because the database does not exist." )
57
+ rescue => e
58
+ log_message ( "Request #{ notification_name } failed:\n #{ e . full_message ( highlight : false ) } " )
23
59
end
24
60
end
25
61
@@ -88,11 +124,8 @@ def initialize(stdout: $stdout, override_default_output_device: true)
88
124
end
89
125
90
126
def start
91
- # Load routes if they haven't been loaded yet (see https://github.com/rails/rails/pull/51614).
92
- routes_reloader = ::Rails . application . routes_reloader
93
- routes_reloader . execute_unless_loaded if routes_reloader &.respond_to? ( :execute_unless_loaded )
94
-
95
- send_message ( { result : { message : "ok" , root : ::Rails . root . to_s } } )
127
+ load_routes
128
+ send_result ( { message : "ok" , root : ::Rails . root . to_s } )
96
129
97
130
while @running
98
131
headers = @stdin . gets ( "\r \n \r \n " )
@@ -104,41 +137,50 @@ def start
104
137
end
105
138
106
139
def execute ( request , params )
107
- request_name = request
108
- request_name = "#{ params [ :server_addon_name ] } ##{ params [ :request_name ] } " if request == "server_addon/delegate"
109
-
110
140
case request
111
141
when "shutdown"
112
142
@running = false
113
143
when "model"
114
- send_message ( resolve_database_info_from_model ( params . fetch ( :name ) ) )
144
+ with_request_error_handling ( request ) do
145
+ send_result ( resolve_database_info_from_model ( params . fetch ( :name ) ) )
146
+ end
115
147
when "association_target_location"
116
- send_message ( resolve_association_target ( params ) )
148
+ with_request_error_handling ( request ) do
149
+ send_result ( resolve_association_target ( params ) )
150
+ end
117
151
when "pending_migrations_message"
118
- send_message ( { result : { pending_migrations_message : pending_migrations_message } } )
152
+ with_request_error_handling ( request ) do
153
+ send_result ( { pending_migrations_message : pending_migrations_message } )
154
+ end
119
155
when "run_migrations"
120
- send_message ( { result : run_migrations } )
156
+ with_request_error_handling ( request ) do
157
+ send_result ( run_migrations )
158
+ end
121
159
when "reload"
122
- ::Rails . application . reloader . reload!
160
+ with_notification_error_handling ( request ) do
161
+ ::Rails . application . reloader . reload!
162
+ end
123
163
when "route_location"
124
- send_message ( route_location ( params . fetch ( :name ) ) )
164
+ with_request_error_handling ( request ) do
165
+ send_result ( route_location ( params . fetch ( :name ) ) )
166
+ end
125
167
when "route_info"
126
- send_message ( resolve_route_info ( params ) )
168
+ with_request_error_handling ( request ) do
169
+ send_result ( resolve_route_info ( params ) )
170
+ end
127
171
when "server_addon/register"
128
- require params [ :server_addon_path ]
129
- ServerAddon . finalize_registrations! ( @stdout )
172
+ with_notification_error_handling ( request ) do
173
+ require params [ :server_addon_path ]
174
+ ServerAddon . finalize_registrations! ( @stdout )
175
+ end
130
176
when "server_addon/delegate"
131
177
server_addon_name = params [ :server_addon_name ]
132
178
request_name = params [ :request_name ]
179
+
180
+ # Do not wrap this in error handlers. Server add-ons need to have the flexibility to choose if they want to
181
+ # include a response or not as part of error handling, so a blanket approach is not appropriate.
133
182
ServerAddon . delegate ( server_addon_name , request_name , params . except ( :request_name , :server_addon_name ) )
134
183
end
135
- # Since this is a common problem, we show a specific error message to the user, instead of the full stack trace.
136
- rescue ActiveRecord ::ConnectionNotEstablished
137
- log_message ( "Request #{ request_name } failed because database connection was not established." )
138
- rescue ActiveRecord ::NoDatabaseError
139
- log_message ( "Request #{ request_name } failed because the database does not exist." )
140
- rescue => e
141
- log_message ( "Request #{ request_name } failed:\n " + e . full_message ( highlight : false ) )
142
184
end
143
185
144
186
private
@@ -156,19 +198,15 @@ def resolve_route_info(requirements)
156
198
end
157
199
158
200
source_location = route &.respond_to? ( :source_location ) && route . source_location
201
+ return unless source_location
159
202
160
- if source_location
161
- file , _ , line = source_location . rpartition ( ":" )
162
- body = {
163
- source_location : [ ::Rails . root . join ( file ) . to_s , line ] ,
164
- verb : route . verb ,
165
- path : route . path . spec . to_s ,
166
- }
203
+ file , _ , line = source_location . rpartition ( ":" )
167
204
168
- { result : body }
169
- else
170
- { result : nil }
171
- end
205
+ {
206
+ source_location : [ ::Rails . root . join ( file ) . to_s , line ] ,
207
+ verb : route . verb ,
208
+ path : route . path . spec . to_s ,
209
+ }
172
210
end
173
211
174
212
# Older versions of Rails don't support `route_source_locations`.
@@ -182,74 +220,48 @@ def route_location(name)
182
220
end
183
221
184
222
match_data = name . match ( /^(.+)(_path|_url)$/ )
185
- return { result : nil } unless match_data
223
+ return unless match_data
186
224
187
225
key = match_data [ 1 ]
188
226
189
227
# A token could match the _path or _url pattern, but not be an actual route.
190
228
route = ::Rails . application . routes . named_routes . get ( key )
191
- return { result : nil } unless route &.source_location
192
-
193
- {
194
- result : {
195
- location : ::Rails . root . join ( route . source_location ) . to_s ,
196
- } ,
197
- }
198
- rescue => e
199
- { error : e . full_message ( highlight : false ) }
229
+ return unless route &.source_location
230
+
231
+ { location : ::Rails . root . join ( route . source_location ) . to_s }
200
232
end
201
233
else
202
234
def route_location ( name )
203
- { result : nil }
235
+ nil
204
236
end
205
237
end
206
238
207
239
def resolve_database_info_from_model ( model_name )
208
240
const = ActiveSupport ::Inflector . safe_constantize ( model_name )
209
- unless active_record_model? ( const )
210
- return {
211
- result : nil ,
212
- }
213
- end
241
+ return unless active_record_model? ( const )
214
242
215
243
info = {
216
- result : {
217
- columns : const . columns . map { |column | [ column . name , column . type , column . default , column . null ] } ,
218
- primary_keys : Array ( const . primary_key ) ,
219
- } ,
244
+ columns : const . columns . map { |column | [ column . name , column . type , column . default , column . null ] } ,
245
+ primary_keys : Array ( const . primary_key ) ,
220
246
}
221
247
222
248
if ActiveRecord ::Tasks ::DatabaseTasks . respond_to? ( :schema_dump_path )
223
- info [ :result ] [ :schema_file ] =
224
- ActiveRecord ::Tasks ::DatabaseTasks . schema_dump_path ( const . connection . pool . db_config )
225
-
249
+ info [ :schema_file ] = ActiveRecord ::Tasks ::DatabaseTasks . schema_dump_path ( const . connection . pool . db_config )
226
250
end
251
+
227
252
info
228
- rescue => e
229
- { error : e . full_message ( highlight : false ) }
230
253
end
231
254
232
255
def resolve_association_target ( params )
233
256
const = ActiveSupport ::Inflector . safe_constantize ( params [ :model_name ] )
234
- unless active_record_model? ( const )
235
- return {
236
- result : nil ,
237
- }
238
- end
257
+ return unless active_record_model? ( const )
239
258
240
259
association_klass = const . reflect_on_association ( params [ :association_name ] . intern ) . klass
241
-
242
260
source_location = Object . const_source_location ( association_klass . to_s )
243
261
244
- {
245
- result : {
246
- location : source_location . first + ":" + source_location . second . to_s ,
247
- } ,
248
- }
262
+ { location : source_location . first + ":" + source_location . second . to_s }
249
263
rescue NameError
250
- {
251
- result : nil ,
252
- }
264
+ nil
253
265
end
254
266
255
267
def active_record_model? ( const )
@@ -282,6 +294,14 @@ def run_migrations
282
294
283
295
{ message : stdout , status : status . exitstatus }
284
296
end
297
+
298
+ def load_routes
299
+ with_notification_error_handling ( "initial_load_routes" ) do
300
+ # Load routes if they haven't been loaded yet (see https://github.com/rails/rails/pull/51614).
301
+ routes_reloader = ::Rails . application . routes_reloader
302
+ routes_reloader . execute_unless_loaded if routes_reloader &.respond_to? ( :execute_unless_loaded )
303
+ end
304
+ end
285
305
end
286
306
end
287
307
end
0 commit comments