spring 1.2.0 → 4.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/LICENSE.txt +3 -2
- data/README.md +148 -42
- data/bin/spring +2 -1
- data/lib/spring/application/boot.rb +11 -4
- data/lib/spring/application.rb +178 -50
- data/lib/spring/application_manager.rb +17 -9
- data/lib/spring/boot.rb +2 -0
- data/lib/spring/client/binstub.rb +44 -45
- data/lib/spring/client/help.rb +1 -1
- data/lib/spring/client/rails.rb +6 -5
- data/lib/spring/client/run.rb +187 -41
- data/lib/spring/client/server.rb +18 -0
- data/lib/spring/client/status.rb +2 -2
- data/lib/spring/client/stop.rb +7 -22
- data/lib/spring/client.rb +11 -1
- data/lib/spring/command_wrapper.rb +0 -4
- data/lib/spring/commands/rails.rb +21 -0
- data/lib/spring/commands.rb +11 -4
- data/lib/spring/configuration.rb +49 -2
- data/lib/spring/env.rb +43 -12
- data/lib/spring/errors.rb +1 -1
- data/lib/spring/failsafe_thread.rb +14 -0
- data/lib/spring/json.rb +30 -33
- data/lib/spring/process_title_updater.rb +2 -2
- data/lib/spring/server.rb +45 -13
- data/lib/spring/version.rb +1 -1
- data/lib/spring/watcher/abstract.rb +44 -13
- data/lib/spring/watcher/polling.rb +48 -11
- data/lib/spring/watcher.rb +1 -1
- metadata +14 -71
- data/.gitignore +0 -19
- data/.travis.yml +0 -11
- data/CHANGELOG.md +0 -188
- data/CONTRIBUTING.md +0 -52
- data/Gemfile +0 -4
- data/Rakefile +0 -21
- data/lib/spring/sid.rb +0 -42
- data/lib/spring/test/acceptance_test.rb +0 -333
- data/lib/spring/test/application.rb +0 -217
- data/lib/spring/test/application_generator.rb +0 -121
- data/lib/spring/test/rails_version.rb +0 -40
- data/lib/spring/test/watcher_test.rb +0 -167
- data/lib/spring/test.rb +0 -15
- data/spring.gemspec +0 -23
- data/test/acceptance_test.rb +0 -4
- data/test/apps/.gitignore +0 -3
- data/test/helper.rb +0 -7
- data/test/unit/client/help_test.rb +0 -60
- data/test/unit/client/version_test.rb +0 -14
- data/test/unit/commands_test.rb +0 -59
- data/test/unit/process_title_updater_test.rb +0 -24
- data/test/unit/watcher_test.rb +0 -9
data/lib/spring/application.rb
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
require "spring/boot"
|
|
2
|
-
require "set"
|
|
3
2
|
require "pty"
|
|
4
3
|
|
|
5
4
|
module Spring
|
|
6
5
|
class Application
|
|
7
6
|
attr_reader :manager, :watcher, :spring_env, :original_env
|
|
8
7
|
|
|
9
|
-
def initialize(manager, original_env)
|
|
8
|
+
def initialize(manager, original_env, spring_env = Env.new)
|
|
10
9
|
@manager = manager
|
|
11
10
|
@original_env = original_env
|
|
12
|
-
@spring_env =
|
|
11
|
+
@spring_env = spring_env
|
|
13
12
|
@mutex = Mutex.new
|
|
14
|
-
@waiting =
|
|
13
|
+
@waiting = {}
|
|
14
|
+
@clients = {}
|
|
15
15
|
@preloaded = false
|
|
16
16
|
@state = :initialized
|
|
17
17
|
@interrupt = IO.pipe
|
|
@@ -28,6 +28,11 @@ module Spring
|
|
|
28
28
|
@interrupt.last.write "."
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
def spawn_env
|
|
32
|
+
env = JSON.load(ENV["SPRING_SPAWN_ENV"].dup).map { |key, value| "#{key}=#{value}" }
|
|
33
|
+
env.join(", ") if env.any?
|
|
34
|
+
end
|
|
35
|
+
|
|
31
36
|
def app_env
|
|
32
37
|
ENV['RAILS_ENV']
|
|
33
38
|
end
|
|
@@ -66,7 +71,17 @@ module Spring
|
|
|
66
71
|
|
|
67
72
|
def start_watcher
|
|
68
73
|
@watcher = Spring.watcher
|
|
69
|
-
|
|
74
|
+
|
|
75
|
+
@watcher.on_stale do
|
|
76
|
+
state! :watcher_stale
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
if @watcher.respond_to? :on_debug
|
|
80
|
+
@watcher.on_debug do |message|
|
|
81
|
+
spring_env.log "[watcher:#{app_env}] #{message}"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
70
85
|
@watcher.start
|
|
71
86
|
end
|
|
72
87
|
|
|
@@ -81,32 +96,52 @@ module Spring
|
|
|
81
96
|
|
|
82
97
|
require Spring.application_root_path.join("config", "application")
|
|
83
98
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
Rails::Application.initializer :
|
|
89
|
-
|
|
99
|
+
unless Rails.respond_to?(:gem_version) && Rails.gem_version >= Gem::Version.new('6.0.0')
|
|
100
|
+
raise "Spring only supports Rails >= 6.0.0"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
Rails::Application.initializer :ensure_reloading_is_enabled, group: :all do
|
|
104
|
+
next if Spring.dangerously_allow_disabling_reloading
|
|
105
|
+
|
|
106
|
+
if Rails.application.config.cache_classes
|
|
107
|
+
config_name, set_to = if Rails.application.config.respond_to?(:enable_reloading=)
|
|
108
|
+
["enable_reloading", "true"]
|
|
109
|
+
else
|
|
110
|
+
["cache_classes", "false"]
|
|
111
|
+
end
|
|
112
|
+
raise <<-MSG.strip_heredoc
|
|
113
|
+
Spring reloads, and therefore needs the application to have reloading enabled.
|
|
114
|
+
Please, set config.#{config_name} to #{set_to} in config/environments/#{Rails.env}.rb.
|
|
115
|
+
(If you understand the trade-offs and want to disable Rails' reloader anyway,
|
|
116
|
+
set `Spring.dangerously_allow_disabling_reloading = true` in config/spring.rb.)
|
|
117
|
+
MSG
|
|
118
|
+
end
|
|
90
119
|
end
|
|
91
120
|
|
|
92
121
|
require Spring.application_root_path.join("config", "environment")
|
|
93
122
|
|
|
94
|
-
|
|
95
|
-
|
|
123
|
+
invoke_after_environment_load_callbacks
|
|
124
|
+
preload_framework_base_classes
|
|
96
125
|
|
|
97
126
|
disconnect_database
|
|
98
127
|
|
|
99
128
|
@preloaded = :success
|
|
100
129
|
rescue Exception => e
|
|
101
130
|
@preloaded = :failure
|
|
102
|
-
watcher.add e.backtrace.map { |line| line
|
|
131
|
+
watcher.add e.backtrace.map { |line| line[/^(.*)\:\d+/, 1] }
|
|
103
132
|
raise e unless initialized?
|
|
104
133
|
ensure
|
|
105
134
|
watcher.add loaded_application_features
|
|
106
|
-
watcher.add Spring.gemfile,
|
|
135
|
+
watcher.add Spring.gemfile, Spring.gemfile_lock
|
|
107
136
|
|
|
108
137
|
if defined?(Rails) && Rails.application
|
|
109
138
|
watcher.add Rails.application.paths["config/initializers"]
|
|
139
|
+
rails_root = Rails.root.to_s
|
|
140
|
+
Rails::Engine.subclasses.each do |engine|
|
|
141
|
+
if engine.root.to_s.start_with?(rails_root)
|
|
142
|
+
watcher.add engine.paths["config/initializers"].expanded
|
|
143
|
+
end
|
|
144
|
+
end
|
|
110
145
|
watcher.add Rails.application.paths["config/database"]
|
|
111
146
|
if secrets_path = Rails.application.paths["config/secrets"]
|
|
112
147
|
watcher.add secrets_path
|
|
@@ -114,8 +149,26 @@ module Spring
|
|
|
114
149
|
end
|
|
115
150
|
end
|
|
116
151
|
|
|
152
|
+
# Eagerly autoload framework base classes
|
|
153
|
+
FRAMEWORK_BASE_CLASSES = %w[
|
|
154
|
+
ActionMailer::Base
|
|
155
|
+
ActionController::Base
|
|
156
|
+
ActionController::API
|
|
157
|
+
].freeze
|
|
158
|
+
|
|
159
|
+
def preload_framework_base_classes
|
|
160
|
+
FRAMEWORK_BASE_CLASSES.each do |const|
|
|
161
|
+
Object.const_get(const) if Object.const_defined?(const)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
117
165
|
def eager_preload
|
|
118
|
-
with_pty
|
|
166
|
+
with_pty do
|
|
167
|
+
# we can't see stderr and there could be issues when it's overflown
|
|
168
|
+
# see https://github.com/rails/spring/issues/396
|
|
169
|
+
STDERR.reopen("/dev/null")
|
|
170
|
+
preload
|
|
171
|
+
end
|
|
119
172
|
end
|
|
120
173
|
|
|
121
174
|
def run
|
|
@@ -137,10 +190,23 @@ module Spring
|
|
|
137
190
|
log "got client"
|
|
138
191
|
manager.puts
|
|
139
192
|
|
|
140
|
-
|
|
193
|
+
@clients[client] = true
|
|
194
|
+
|
|
195
|
+
_stdout, stderr, _stdin = streams = 3.times.map { client.recv_io }
|
|
141
196
|
[STDOUT, STDERR, STDIN].zip(streams).each { |a, b| a.reopen(b) }
|
|
142
197
|
|
|
143
|
-
|
|
198
|
+
if preloaded?
|
|
199
|
+
client.puts(0) # preload success
|
|
200
|
+
else
|
|
201
|
+
begin
|
|
202
|
+
preload
|
|
203
|
+
client.puts(0) # preload success
|
|
204
|
+
rescue Exception
|
|
205
|
+
log "preload failed"
|
|
206
|
+
ignore_client_disconnect { client.puts(1) } # preload failure
|
|
207
|
+
raise
|
|
208
|
+
end
|
|
209
|
+
end
|
|
144
210
|
|
|
145
211
|
args, env = JSON.load(client.read(client.gets.to_i)).values_at("args", "env")
|
|
146
212
|
command = Spring.command(args.shift)
|
|
@@ -149,31 +215,39 @@ module Spring
|
|
|
149
215
|
setup command
|
|
150
216
|
|
|
151
217
|
if Rails.application.reloaders.any?(&:updated?)
|
|
152
|
-
|
|
153
|
-
ActionDispatch::Reloader.prepare!
|
|
218
|
+
Rails.application.reloader.reload!
|
|
154
219
|
end
|
|
155
220
|
|
|
156
221
|
pid = fork {
|
|
222
|
+
# Make sure to close other clients otherwise their graceful termination
|
|
223
|
+
# will be impossible due to reference from this fork.
|
|
224
|
+
@clients.each_key { |c| c.close if c != client }
|
|
225
|
+
|
|
226
|
+
Process.setsid
|
|
157
227
|
IGNORE_SIGNALS.each { |sig| trap(sig, "DEFAULT") }
|
|
158
228
|
trap("TERM", "DEFAULT")
|
|
159
229
|
|
|
230
|
+
unless Spring.quiet
|
|
231
|
+
STDERR.puts "Running via Spring preloader in process #{Process.pid}"
|
|
232
|
+
|
|
233
|
+
if Rails.env.production?
|
|
234
|
+
STDERR.puts "WARNING: Spring is running in production. To fix " \
|
|
235
|
+
"this make sure the spring gem is only present " \
|
|
236
|
+
"in `development` and `test` groups in your Gemfile " \
|
|
237
|
+
"and make sure you always use " \
|
|
238
|
+
"`bundle install --without development test` in production"
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
160
242
|
ARGV.replace(args)
|
|
161
|
-
$0 = command.
|
|
243
|
+
$0 = command.exec_name
|
|
162
244
|
|
|
163
|
-
# Delete all env vars which are unchanged from before
|
|
245
|
+
# Delete all env vars which are unchanged from before Spring started
|
|
164
246
|
original_env.each { |k, v| ENV.delete k if ENV[k] == v }
|
|
165
247
|
|
|
166
|
-
# Load in the current env vars, except those which *were* changed when
|
|
248
|
+
# Load in the current env vars, except those which *were* changed when Spring started
|
|
167
249
|
env.each { |k, v| ENV[k] ||= v }
|
|
168
250
|
|
|
169
|
-
# requiring is faster, so if config.cache_classes was true in
|
|
170
|
-
# the environment's config file, then we can respect that from
|
|
171
|
-
# here on as we no longer need constant reloading.
|
|
172
|
-
if @original_cache_classes
|
|
173
|
-
ActiveSupport::Dependencies.mechanism = :require
|
|
174
|
-
Rails.application.config.cache_classes = true
|
|
175
|
-
end
|
|
176
|
-
|
|
177
251
|
connect_database
|
|
178
252
|
srand
|
|
179
253
|
|
|
@@ -184,30 +258,38 @@ module Spring
|
|
|
184
258
|
}
|
|
185
259
|
|
|
186
260
|
disconnect_database
|
|
187
|
-
reset_streams
|
|
188
261
|
|
|
189
262
|
log "forked #{pid}"
|
|
190
263
|
manager.puts pid
|
|
191
264
|
|
|
192
265
|
wait pid, streams, client
|
|
193
266
|
rescue Exception => e
|
|
194
|
-
|
|
267
|
+
if e.is_a?(Errno::EPIPE)
|
|
268
|
+
log "client disconnected (#{e.message}), ignoring command"
|
|
269
|
+
else
|
|
270
|
+
log "exception: #{e}"
|
|
271
|
+
end
|
|
195
272
|
manager.puts unless pid
|
|
196
273
|
|
|
197
274
|
if streams && !e.is_a?(SystemExit)
|
|
198
|
-
print_exception(stderr, e)
|
|
199
|
-
streams.each
|
|
275
|
+
ignore_client_disconnect { print_exception(stderr, e) }
|
|
276
|
+
streams.each { |stream| ignore_client_disconnect { stream.close } }
|
|
200
277
|
end
|
|
201
278
|
|
|
202
|
-
client.puts(1) if pid
|
|
279
|
+
ignore_client_disconnect { client.puts(1) if pid }
|
|
203
280
|
client.close
|
|
281
|
+
ensure
|
|
282
|
+
# Redirect STDOUT and STDERR to prevent from keeping the original FDs
|
|
283
|
+
# (i.e. to prevent `spring rake -T | grep db` from hanging forever),
|
|
284
|
+
# even when exception is raised before forking (i.e. preloading).
|
|
285
|
+
reset_streams
|
|
204
286
|
end
|
|
205
287
|
|
|
206
288
|
def terminate
|
|
207
289
|
if exiting?
|
|
208
290
|
# Ensure that we do not ignore subsequent termination attempts
|
|
209
291
|
log "forced exit"
|
|
210
|
-
@waiting.
|
|
292
|
+
@waiting.each_key { |pid| Process.kill("TERM", pid) }
|
|
211
293
|
Kernel.exit
|
|
212
294
|
else
|
|
213
295
|
state! :terminating
|
|
@@ -242,6 +324,12 @@ module Spring
|
|
|
242
324
|
end
|
|
243
325
|
end
|
|
244
326
|
|
|
327
|
+
def invoke_after_environment_load_callbacks
|
|
328
|
+
Spring.after_environment_load_callbacks.each do |callback|
|
|
329
|
+
callback.call
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
245
333
|
def loaded_application_features
|
|
246
334
|
root = Spring.application_root_path.to_s
|
|
247
335
|
$LOADED_FEATURES.select { |f| f.start_with?(root) }
|
|
@@ -260,13 +348,28 @@ module Spring
|
|
|
260
348
|
Kernel.module_eval do
|
|
261
349
|
old_raise = Kernel.method(:raise)
|
|
262
350
|
remove_method :raise
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
351
|
+
if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.2.0')
|
|
352
|
+
define_method :raise do |*args, **kwargs|
|
|
353
|
+
begin
|
|
354
|
+
old_raise.call(*args, **kwargs)
|
|
355
|
+
ensure
|
|
356
|
+
if $!
|
|
357
|
+
lib = File.expand_path("..", __FILE__)
|
|
358
|
+
$!.backtrace.reject! { |line| line.start_with?(lib) } unless $!.backtrace.frozen?
|
|
359
|
+
$!.backtrace_locations.reject! { |line| line.path&.start_with?(lib) } unless $!.backtrace_locations.frozen?
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
else
|
|
364
|
+
define_method :raise do |*args|
|
|
365
|
+
begin
|
|
366
|
+
old_raise.call(*args)
|
|
367
|
+
ensure
|
|
368
|
+
if $!
|
|
369
|
+
lib = File.expand_path("..", __FILE__)
|
|
370
|
+
$!.backtrace.reject! { |line| line.start_with?(lib) } unless $!.backtrace.frozen?
|
|
371
|
+
$!.backtrace_locations.reject! { |line| line.path&.start_with?(lib) } unless $!.backtrace_locations.frozen?
|
|
372
|
+
end
|
|
270
373
|
end
|
|
271
374
|
end
|
|
272
375
|
end
|
|
@@ -283,8 +386,13 @@ module Spring
|
|
|
283
386
|
def with_pty
|
|
284
387
|
PTY.open do |master, slave|
|
|
285
388
|
[STDOUT, STDERR, STDIN].each { |s| s.reopen slave }
|
|
286
|
-
|
|
287
|
-
|
|
389
|
+
reader_thread = Spring.failsafe_thread { master.read }
|
|
390
|
+
begin
|
|
391
|
+
yield
|
|
392
|
+
ensure
|
|
393
|
+
reader_thread.kill
|
|
394
|
+
reset_streams
|
|
395
|
+
end
|
|
288
396
|
end
|
|
289
397
|
end
|
|
290
398
|
|
|
@@ -294,26 +402,46 @@ module Spring
|
|
|
294
402
|
end
|
|
295
403
|
|
|
296
404
|
def wait(pid, streams, client)
|
|
297
|
-
@mutex.synchronize { @waiting
|
|
405
|
+
@mutex.synchronize { @waiting[pid] = true }
|
|
298
406
|
|
|
299
407
|
# Wait in a separate thread so we can run multiple commands at once
|
|
300
|
-
|
|
408
|
+
Spring.failsafe_thread {
|
|
301
409
|
begin
|
|
302
410
|
_, status = Process.wait2 pid
|
|
303
|
-
log "#{pid} exited with #{status.exitstatus}"
|
|
411
|
+
log "#{pid} exited with #{status.exitstatus || status.inspect}"
|
|
304
412
|
|
|
305
413
|
streams.each(&:close)
|
|
306
|
-
client.puts(status.exitstatus)
|
|
414
|
+
client.puts(status.exitstatus || status.to_i)
|
|
307
415
|
client.close
|
|
308
416
|
ensure
|
|
309
417
|
@mutex.synchronize { @waiting.delete pid }
|
|
310
418
|
exit_if_finished
|
|
311
419
|
end
|
|
312
420
|
}
|
|
421
|
+
|
|
422
|
+
Spring.failsafe_thread {
|
|
423
|
+
while signal = client.gets.chomp
|
|
424
|
+
begin
|
|
425
|
+
Process.kill(signal, -Process.getpgid(pid))
|
|
426
|
+
client.puts(0)
|
|
427
|
+
rescue Errno::ESRCH
|
|
428
|
+
client.puts(1)
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
}
|
|
313
432
|
end
|
|
314
433
|
|
|
315
434
|
private
|
|
316
435
|
|
|
436
|
+
# Tolerate Errno::EPIPE on writes to the client socket. Once the client
|
|
437
|
+
# disconnects, every subsequent write to it raises — that's expected
|
|
438
|
+
# during `serve`'s status reporting; we'd be talking to a process
|
|
439
|
+
# that's gone.
|
|
440
|
+
def ignore_client_disconnect
|
|
441
|
+
yield
|
|
442
|
+
rescue Errno::EPIPE
|
|
443
|
+
end
|
|
444
|
+
|
|
317
445
|
def active_record_configured?
|
|
318
446
|
defined?(ActiveRecord::Base) && ActiveRecord::Base.configurations.any?
|
|
319
447
|
end
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
module Spring
|
|
2
2
|
class ApplicationManager
|
|
3
|
-
attr_reader :pid, :child, :app_env, :spring_env, :status
|
|
3
|
+
attr_reader :pid, :child, :app_env, :spawn_env, :spring_env, :status
|
|
4
4
|
|
|
5
|
-
def initialize(app_env)
|
|
5
|
+
def initialize(app_env, spawn_env, spring_env)
|
|
6
6
|
@app_env = app_env
|
|
7
|
-
@
|
|
7
|
+
@spawn_env = spawn_env
|
|
8
|
+
@spring_env = spring_env
|
|
8
9
|
@mutex = Mutex.new
|
|
9
10
|
@state = :running
|
|
11
|
+
@pid = nil
|
|
10
12
|
end
|
|
11
13
|
|
|
12
14
|
def log(message)
|
|
@@ -40,7 +42,7 @@ module Spring
|
|
|
40
42
|
if alive?
|
|
41
43
|
begin
|
|
42
44
|
yield
|
|
43
|
-
rescue Errno::ECONNRESET, Errno::EPIPE
|
|
45
|
+
rescue Errno::ECONNRESET, Errno::EPIPE, Errno::EINVAL
|
|
44
46
|
# The child has died but has not been collected by the wait thread yet,
|
|
45
47
|
# so start a new child and try again.
|
|
46
48
|
log "child dead; starting"
|
|
@@ -92,18 +94,23 @@ module Spring
|
|
|
92
94
|
def start_child(preload = false)
|
|
93
95
|
@child, child_socket = UNIXSocket.pair
|
|
94
96
|
|
|
95
|
-
Bundler.
|
|
97
|
+
Bundler.with_original_env do
|
|
98
|
+
bundler_dir = File.expand_path("../..", $LOADED_FEATURES.grep(/bundler\/setup\.rb$/).first)
|
|
96
99
|
@pid = Process.spawn(
|
|
97
100
|
{
|
|
98
101
|
"RAILS_ENV" => app_env,
|
|
99
102
|
"RACK_ENV" => app_env,
|
|
100
103
|
"SPRING_ORIGINAL_ENV" => JSON.dump(Spring::ORIGINAL_ENV),
|
|
101
|
-
"SPRING_PRELOAD" => preload ? "1" : "0"
|
|
104
|
+
"SPRING_PRELOAD" => preload ? "1" : "0",
|
|
105
|
+
"SPRING_SPAWN_ENV" => JSON.dump(spawn_env.compact),
|
|
106
|
+
**spawn_env,
|
|
102
107
|
},
|
|
103
108
|
"ruby",
|
|
109
|
+
*(bundler_dir != RbConfig::CONFIG["rubylibdir"] ? ["-I", bundler_dir] : []),
|
|
104
110
|
"-I", File.expand_path("../..", __FILE__),
|
|
105
111
|
"-e", "require 'spring/application/boot'",
|
|
106
|
-
3 => child_socket
|
|
112
|
+
3 => child_socket,
|
|
113
|
+
4 => spring_env.log_file,
|
|
107
114
|
)
|
|
108
115
|
end
|
|
109
116
|
|
|
@@ -114,12 +121,13 @@ module Spring
|
|
|
114
121
|
def start_wait_thread(pid, child)
|
|
115
122
|
Process.detach(pid)
|
|
116
123
|
|
|
117
|
-
|
|
124
|
+
Spring.failsafe_thread {
|
|
118
125
|
# The recv can raise an ECONNRESET, killing the thread, but that's ok
|
|
119
126
|
# as if it does we're no longer interested in the child
|
|
120
127
|
loop do
|
|
121
128
|
IO.select([child])
|
|
122
|
-
|
|
129
|
+
peek = child.recv(1, Socket::MSG_PEEK)
|
|
130
|
+
break if peek.nil? || peek.empty?
|
|
123
131
|
sleep 0.01
|
|
124
132
|
end
|
|
125
133
|
|
data/lib/spring/boot.rb
CHANGED
|
@@ -1,54 +1,49 @@
|
|
|
1
|
-
require 'set'
|
|
2
|
-
|
|
3
1
|
module Spring
|
|
4
2
|
module Client
|
|
5
3
|
class Binstub < Command
|
|
6
|
-
SHEBANG = /\#\!.*\n
|
|
4
|
+
SHEBANG = /\#\!.*\n(\#.*\n)*/
|
|
7
5
|
|
|
8
|
-
# If loading the bin/spring file works, it'll run
|
|
6
|
+
# If loading the bin/spring file works, it'll run Spring which will
|
|
9
7
|
# eventually call Kernel.exit. This means that in the client process
|
|
10
|
-
# we will never execute the lines after this block. But if the
|
|
8
|
+
# we will never execute the lines after this block. But if the Spring
|
|
11
9
|
# client is not invoked for whatever reason, then the Kernel.exit won't
|
|
12
10
|
# happen, and so we'll fall back to the lines after this block, which
|
|
13
11
|
# should cause the "unsprung" version of the command to run.
|
|
14
|
-
LOADER =
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
rescue LoadError
|
|
18
|
-
end
|
|
19
|
-
CODE
|
|
12
|
+
LOADER = <<~CODE
|
|
13
|
+
load File.expand_path("spring", __dir__)
|
|
14
|
+
CODE
|
|
20
15
|
|
|
21
16
|
# The defined? check ensures these lines don't execute when we load the
|
|
22
17
|
# binstub from the application process. Which means that in the application
|
|
23
18
|
# process we'll execute the lines which come after the LOADER block, which
|
|
24
19
|
# is what we want.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
ENV["GEM_HOME"] = ""
|
|
42
|
-
Gem.paths = ENV
|
|
43
|
-
|
|
44
|
-
gem "spring", match[1]
|
|
45
|
-
require "spring/binstub"
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
CODE
|
|
20
|
+
SPRING = <<~CODE
|
|
21
|
+
#!/usr/bin/env ruby
|
|
22
|
+
|
|
23
|
+
# This file loads Spring without loading other gems in the Gemfile in order to be fast.
|
|
24
|
+
# It gets overwritten when you run the `spring binstub` command.
|
|
25
|
+
|
|
26
|
+
if !defined?(Spring) && [nil, "development", "test"].include?(ENV["RAILS_ENV"])
|
|
27
|
+
require "bundler"
|
|
28
|
+
|
|
29
|
+
Bundler.definition.requested_specs.find { |spec| spec.name == "spring" }&.tap do |spring|
|
|
30
|
+
Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path
|
|
31
|
+
gem "spring", spring.version
|
|
32
|
+
require "spring/binstub"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
CODE
|
|
49
36
|
|
|
50
37
|
OLD_BINSTUB = %{if !Process.respond_to?(:fork) || Gem::Specification.find_all_by_name("spring").empty?}
|
|
51
38
|
|
|
39
|
+
BINSTUB_VARIATIONS = Regexp.union [
|
|
40
|
+
%{load File.expand_path("spring", __dir__)\n},
|
|
41
|
+
%{begin\n load File.expand_path('../spring', __FILE__)\nrescue LoadError => e\n raise unless e.message.include?('spring')\nend\n},
|
|
42
|
+
%{begin\n load File.expand_path('../spring', __FILE__)\nrescue LoadError\nend\n},
|
|
43
|
+
%{begin\n spring_bin_path = File.expand_path('../spring', __FILE__)\n load spring_bin_path\nrescue LoadError => e\n raise unless e.message.end_with? spring_bin_path, 'spring/binstub'\nend\n},
|
|
44
|
+
LOADER
|
|
45
|
+
].map { |binstub| /#{Regexp.escape(binstub).gsub("'", "['\"]")}/ }
|
|
46
|
+
|
|
52
47
|
class Item
|
|
53
48
|
attr_reader :command, :existing
|
|
54
49
|
|
|
@@ -75,8 +70,12 @@ CODE
|
|
|
75
70
|
fallback = nil if fallback.include?("exec")
|
|
76
71
|
generate(fallback)
|
|
77
72
|
status "upgraded"
|
|
78
|
-
elsif existing
|
|
79
|
-
status "
|
|
73
|
+
elsif existing.include?(LOADER)
|
|
74
|
+
status "Spring already present"
|
|
75
|
+
elsif existing =~ BINSTUB_VARIATIONS
|
|
76
|
+
upgraded = existing.sub(BINSTUB_VARIATIONS, LOADER)
|
|
77
|
+
File.write(command.binstub, upgraded)
|
|
78
|
+
status "upgraded"
|
|
80
79
|
else
|
|
81
80
|
head, shebang, tail = existing.partition(SHEBANG)
|
|
82
81
|
|
|
@@ -87,15 +86,15 @@ CODE
|
|
|
87
86
|
end
|
|
88
87
|
|
|
89
88
|
File.write(command.binstub, "#{head}#{shebang}#{LOADER}#{tail}")
|
|
90
|
-
status "
|
|
89
|
+
status "Spring inserted"
|
|
91
90
|
else
|
|
92
|
-
status "doesn't appear to be ruby, so cannot use
|
|
91
|
+
status "doesn't appear to be ruby, so cannot use Spring", $stderr
|
|
93
92
|
exit 1
|
|
94
93
|
end
|
|
95
94
|
end
|
|
96
95
|
else
|
|
97
96
|
generate
|
|
98
|
-
status "generated with
|
|
97
|
+
status "generated with Spring"
|
|
99
98
|
end
|
|
100
99
|
end
|
|
101
100
|
|
|
@@ -111,8 +110,8 @@ CODE
|
|
|
111
110
|
|
|
112
111
|
def remove
|
|
113
112
|
if existing
|
|
114
|
-
File.write(command.binstub, existing.sub(
|
|
115
|
-
status "
|
|
113
|
+
File.write(command.binstub, existing.sub(BINSTUB_VARIATIONS, ""))
|
|
114
|
+
status "Spring removed"
|
|
116
115
|
end
|
|
117
116
|
end
|
|
118
117
|
end
|
|
@@ -120,7 +119,7 @@ CODE
|
|
|
120
119
|
attr_reader :bindir, :items
|
|
121
120
|
|
|
122
121
|
def self.description
|
|
123
|
-
"Generate
|
|
122
|
+
"Generate Spring based binstubs. Use --all to generate a binstub for all known commands. Use --remove to revert."
|
|
124
123
|
end
|
|
125
124
|
|
|
126
125
|
def self.rails_command
|
|
@@ -140,7 +139,7 @@ CODE
|
|
|
140
139
|
@mode = :add
|
|
141
140
|
@items = args.drop(1)
|
|
142
141
|
.map { |name| find_commands name }
|
|
143
|
-
.
|
|
142
|
+
.flatten.uniq
|
|
144
143
|
.map { |command| Item.new(command) }
|
|
145
144
|
end
|
|
146
145
|
|
|
@@ -149,7 +148,7 @@ CODE
|
|
|
149
148
|
when "--all"
|
|
150
149
|
@all = true
|
|
151
150
|
commands = Spring.commands.dup
|
|
152
|
-
commands.delete_if { |
|
|
151
|
+
commands.delete_if { |command_name, _| command_name.start_with?("rails_") }
|
|
153
152
|
commands.values + [self.class.rails_command]
|
|
154
153
|
when "--remove"
|
|
155
154
|
@mode = :remove
|
data/lib/spring/client/help.rb
CHANGED
|
@@ -32,7 +32,7 @@ module Spring
|
|
|
32
32
|
def formatted_help
|
|
33
33
|
["Version: #{env.version}\n",
|
|
34
34
|
"Usage: spring COMMAND [ARGS]\n",
|
|
35
|
-
*command_help("
|
|
35
|
+
*command_help("Spring itself", spring_commands),
|
|
36
36
|
'',
|
|
37
37
|
*command_help("your application", application_commands)].join("\n")
|
|
38
38
|
end
|
data/lib/spring/client/rails.rb
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
|
-
require "set"
|
|
2
|
-
|
|
3
1
|
module Spring
|
|
4
2
|
module Client
|
|
5
3
|
class Rails < Command
|
|
6
|
-
COMMANDS =
|
|
4
|
+
COMMANDS = %w(console runner generate destroy test)
|
|
7
5
|
|
|
8
6
|
ALIASES = {
|
|
9
7
|
"c" => "console",
|
|
10
8
|
"r" => "runner",
|
|
11
9
|
"g" => "generate",
|
|
12
|
-
"d" => "destroy"
|
|
10
|
+
"d" => "destroy",
|
|
11
|
+
"t" => "test"
|
|
13
12
|
}
|
|
14
13
|
|
|
15
14
|
def self.description
|
|
16
|
-
"Run a rails command. The following sub commands will use
|
|
15
|
+
"Run a rails command. The following sub commands will use Spring: #{COMMANDS.to_a.join ', '}."
|
|
17
16
|
end
|
|
18
17
|
|
|
19
18
|
def call
|
|
@@ -21,6 +20,8 @@ module Spring
|
|
|
21
20
|
|
|
22
21
|
if COMMANDS.include?(command_name)
|
|
23
22
|
Run.call(["rails_#{command_name}", *args.drop(2)])
|
|
23
|
+
elsif command_name&.start_with?("db:") && !command_name.start_with?("db:system")
|
|
24
|
+
Run.call(["rake", *args.drop(1)])
|
|
24
25
|
else
|
|
25
26
|
require "spring/configuration"
|
|
26
27
|
ARGV.shift
|