mini-clean-kit 0.0.1
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 +7 -0
- data/mini-clean-kit.gemspec +12 -0
- data/spring-4.7.0/LICENSE.txt +23 -0
- data/spring-4.7.0/README.md +467 -0
- data/spring-4.7.0/bin/spring +49 -0
- data/spring-4.7.0/lib/spring/application/boot.rb +25 -0
- data/spring-4.7.0/lib/spring/application.rb +449 -0
- data/spring-4.7.0/lib/spring/application_manager.rb +145 -0
- data/spring-4.7.0/lib/spring/binstub.rb +13 -0
- data/spring-4.7.0/lib/spring/boot.rb +10 -0
- data/spring-4.7.0/lib/spring/client/binstub.rb +190 -0
- data/spring-4.7.0/lib/spring/client/command.rb +18 -0
- data/spring-4.7.0/lib/spring/client/help.rb +62 -0
- data/spring-4.7.0/lib/spring/client/rails.rb +34 -0
- data/spring-4.7.0/lib/spring/client/run.rb +297 -0
- data/spring-4.7.0/lib/spring/client/server.rb +18 -0
- data/spring-4.7.0/lib/spring/client/status.rb +30 -0
- data/spring-4.7.0/lib/spring/client/stop.rb +22 -0
- data/spring-4.7.0/lib/spring/client/version.rb +11 -0
- data/spring-4.7.0/lib/spring/client.rb +48 -0
- data/spring-4.7.0/lib/spring/command_wrapper.rb +82 -0
- data/spring-4.7.0/lib/spring/commands/rails.rb +112 -0
- data/spring-4.7.0/lib/spring/commands/rake.rb +30 -0
- data/spring-4.7.0/lib/spring/commands.rb +57 -0
- data/spring-4.7.0/lib/spring/configuration.rb +99 -0
- data/spring-4.7.0/lib/spring/env.rb +115 -0
- data/spring-4.7.0/lib/spring/errors.rb +36 -0
- data/spring-4.7.0/lib/spring/failsafe_thread.rb +14 -0
- data/spring-4.7.0/lib/spring/json.rb +623 -0
- data/spring-4.7.0/lib/spring/process_title_updater.rb +65 -0
- data/spring-4.7.0/lib/spring/server.rb +162 -0
- data/spring-4.7.0/lib/spring/version.rb +3 -0
- data/spring-4.7.0/lib/spring/watcher/abstract.rb +117 -0
- data/spring-4.7.0/lib/spring/watcher/polling.rb +98 -0
- data/spring-4.7.0/lib/spring/watcher.rb +30 -0
- metadata +75 -0
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
require "spring/boot"
|
|
2
|
+
require "pty"
|
|
3
|
+
|
|
4
|
+
module Spring
|
|
5
|
+
class Application
|
|
6
|
+
attr_reader :manager, :watcher, :spring_env, :original_env
|
|
7
|
+
|
|
8
|
+
def initialize(manager, original_env, spring_env = Env.new)
|
|
9
|
+
@manager = manager
|
|
10
|
+
@original_env = original_env
|
|
11
|
+
@spring_env = spring_env
|
|
12
|
+
@mutex = Mutex.new
|
|
13
|
+
@waiting = {}
|
|
14
|
+
@clients = {}
|
|
15
|
+
@preloaded = false
|
|
16
|
+
@state = :initialized
|
|
17
|
+
@interrupt = IO.pipe
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def state(val)
|
|
21
|
+
return if exiting?
|
|
22
|
+
log "#{@state} -> #{val}"
|
|
23
|
+
@state = val
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def state!(val)
|
|
27
|
+
state val
|
|
28
|
+
@interrupt.last.write "."
|
|
29
|
+
end
|
|
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
|
+
|
|
36
|
+
def app_env
|
|
37
|
+
ENV['RAILS_ENV']
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def app_name
|
|
41
|
+
spring_env.app_name
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def log(message)
|
|
45
|
+
spring_env.log "[application:#{app_env}] #{message}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def preloaded?
|
|
49
|
+
@preloaded
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def preload_failed?
|
|
53
|
+
@preloaded == :failure
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def exiting?
|
|
57
|
+
@state == :exiting
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def terminating?
|
|
61
|
+
@state == :terminating
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def watcher_stale?
|
|
65
|
+
@state == :watcher_stale
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def initialized?
|
|
69
|
+
@state == :initialized
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def start_watcher
|
|
73
|
+
@watcher = Spring.watcher
|
|
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
|
+
|
|
85
|
+
@watcher.start
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def preload
|
|
89
|
+
log "preloading app"
|
|
90
|
+
|
|
91
|
+
begin
|
|
92
|
+
require "spring/commands"
|
|
93
|
+
ensure
|
|
94
|
+
start_watcher
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
require Spring.application_root_path.join("config", "application")
|
|
98
|
+
|
|
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
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
require Spring.application_root_path.join("config", "environment")
|
|
122
|
+
|
|
123
|
+
invoke_after_environment_load_callbacks
|
|
124
|
+
preload_framework_base_classes
|
|
125
|
+
|
|
126
|
+
disconnect_database
|
|
127
|
+
|
|
128
|
+
@preloaded = :success
|
|
129
|
+
rescue Exception => e
|
|
130
|
+
@preloaded = :failure
|
|
131
|
+
watcher.add e.backtrace.map { |line| line[/^(.*)\:\d+/, 1] }
|
|
132
|
+
raise e unless initialized?
|
|
133
|
+
ensure
|
|
134
|
+
watcher.add loaded_application_features
|
|
135
|
+
watcher.add Spring.gemfile, Spring.gemfile_lock
|
|
136
|
+
|
|
137
|
+
if defined?(Rails) && Rails.application
|
|
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
|
|
145
|
+
watcher.add Rails.application.paths["config/database"]
|
|
146
|
+
if secrets_path = Rails.application.paths["config/secrets"]
|
|
147
|
+
watcher.add secrets_path
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
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
|
+
|
|
165
|
+
def eager_preload
|
|
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
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def run
|
|
175
|
+
state :running
|
|
176
|
+
manager.puts
|
|
177
|
+
|
|
178
|
+
loop do
|
|
179
|
+
IO.select [manager, @interrupt.first]
|
|
180
|
+
|
|
181
|
+
if terminating? || watcher_stale? || preload_failed?
|
|
182
|
+
exit
|
|
183
|
+
else
|
|
184
|
+
serve manager.recv_io(UNIXSocket)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def serve(client)
|
|
190
|
+
log "got client"
|
|
191
|
+
manager.puts
|
|
192
|
+
|
|
193
|
+
@clients[client] = true
|
|
194
|
+
|
|
195
|
+
_stdout, stderr, _stdin = streams = 3.times.map { client.recv_io }
|
|
196
|
+
[STDOUT, STDERR, STDIN].zip(streams).each { |a, b| a.reopen(b) }
|
|
197
|
+
|
|
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
|
|
210
|
+
|
|
211
|
+
args, env = JSON.load(client.read(client.gets.to_i)).values_at("args", "env")
|
|
212
|
+
command = Spring.command(args.shift)
|
|
213
|
+
|
|
214
|
+
connect_database
|
|
215
|
+
setup command
|
|
216
|
+
|
|
217
|
+
if Rails.application.reloaders.any?(&:updated?)
|
|
218
|
+
Rails.application.reloader.reload!
|
|
219
|
+
end
|
|
220
|
+
|
|
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
|
|
227
|
+
IGNORE_SIGNALS.each { |sig| trap(sig, "DEFAULT") }
|
|
228
|
+
trap("TERM", "DEFAULT")
|
|
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
|
+
|
|
242
|
+
ARGV.replace(args)
|
|
243
|
+
$0 = command.exec_name
|
|
244
|
+
|
|
245
|
+
# Delete all env vars which are unchanged from before Spring started
|
|
246
|
+
original_env.each { |k, v| ENV.delete k if ENV[k] == v }
|
|
247
|
+
|
|
248
|
+
# Load in the current env vars, except those which *were* changed when Spring started
|
|
249
|
+
env.each { |k, v| ENV[k] ||= v }
|
|
250
|
+
|
|
251
|
+
connect_database
|
|
252
|
+
srand
|
|
253
|
+
|
|
254
|
+
invoke_after_fork_callbacks
|
|
255
|
+
shush_backtraces
|
|
256
|
+
|
|
257
|
+
command.call
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
disconnect_database
|
|
261
|
+
|
|
262
|
+
log "forked #{pid}"
|
|
263
|
+
manager.puts pid
|
|
264
|
+
|
|
265
|
+
wait pid, streams, client
|
|
266
|
+
rescue Exception => e
|
|
267
|
+
if e.is_a?(Errno::EPIPE)
|
|
268
|
+
log "client disconnected (#{e.message}), ignoring command"
|
|
269
|
+
else
|
|
270
|
+
log "exception: #{e}"
|
|
271
|
+
end
|
|
272
|
+
manager.puts unless pid
|
|
273
|
+
|
|
274
|
+
if streams && !e.is_a?(SystemExit)
|
|
275
|
+
ignore_client_disconnect { print_exception(stderr, e) }
|
|
276
|
+
streams.each { |stream| ignore_client_disconnect { stream.close } }
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
ignore_client_disconnect { client.puts(1) if pid }
|
|
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
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def terminate
|
|
289
|
+
if exiting?
|
|
290
|
+
# Ensure that we do not ignore subsequent termination attempts
|
|
291
|
+
log "forced exit"
|
|
292
|
+
@waiting.each_key { |pid| Process.kill("TERM", pid) }
|
|
293
|
+
Kernel.exit
|
|
294
|
+
else
|
|
295
|
+
state! :terminating
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def exit
|
|
300
|
+
state :exiting
|
|
301
|
+
manager.shutdown(:RDWR)
|
|
302
|
+
exit_if_finished
|
|
303
|
+
sleep
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def exit_if_finished
|
|
307
|
+
@mutex.synchronize {
|
|
308
|
+
Kernel.exit if exiting? && @waiting.empty?
|
|
309
|
+
}
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# The command might need to require some files in the
|
|
313
|
+
# main process so that they are cached. For example a test command wants to
|
|
314
|
+
# load the helper file once and have it cached.
|
|
315
|
+
def setup(command)
|
|
316
|
+
if command.setup
|
|
317
|
+
watcher.add loaded_application_features # loaded features may have changed
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def invoke_after_fork_callbacks
|
|
322
|
+
Spring.after_fork_callbacks.each do |callback|
|
|
323
|
+
callback.call
|
|
324
|
+
end
|
|
325
|
+
end
|
|
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
|
+
|
|
333
|
+
def loaded_application_features
|
|
334
|
+
root = Spring.application_root_path.to_s
|
|
335
|
+
$LOADED_FEATURES.select { |f| f.start_with?(root) }
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def disconnect_database
|
|
339
|
+
ActiveRecord::Base.remove_connection if active_record_configured?
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def connect_database
|
|
343
|
+
ActiveRecord::Base.establish_connection if active_record_configured?
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# This feels very naughty
|
|
347
|
+
def shush_backtraces
|
|
348
|
+
Kernel.module_eval do
|
|
349
|
+
old_raise = Kernel.method(:raise)
|
|
350
|
+
remove_method :raise
|
|
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
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
private :raise
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def print_exception(stream, error)
|
|
381
|
+
first, rest = error.backtrace.first, error.backtrace.drop(1)
|
|
382
|
+
stream.puts("#{first}: #{error} (#{error.class})")
|
|
383
|
+
rest.each { |line| stream.puts("\tfrom #{line}") }
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def with_pty
|
|
387
|
+
PTY.open do |master, slave|
|
|
388
|
+
[STDOUT, STDERR, STDIN].each { |s| s.reopen slave }
|
|
389
|
+
reader_thread = Spring.failsafe_thread { master.read }
|
|
390
|
+
begin
|
|
391
|
+
yield
|
|
392
|
+
ensure
|
|
393
|
+
reader_thread.kill
|
|
394
|
+
reset_streams
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def reset_streams
|
|
400
|
+
[STDOUT, STDERR].each { |stream| stream.reopen(spring_env.log_file) }
|
|
401
|
+
STDIN.reopen("/dev/null")
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def wait(pid, streams, client)
|
|
405
|
+
@mutex.synchronize { @waiting[pid] = true }
|
|
406
|
+
|
|
407
|
+
# Wait in a separate thread so we can run multiple commands at once
|
|
408
|
+
Spring.failsafe_thread {
|
|
409
|
+
begin
|
|
410
|
+
_, status = Process.wait2 pid
|
|
411
|
+
log "#{pid} exited with #{status.exitstatus || status.inspect}"
|
|
412
|
+
|
|
413
|
+
streams.each(&:close)
|
|
414
|
+
client.puts(status.exitstatus || status.to_i)
|
|
415
|
+
client.close
|
|
416
|
+
ensure
|
|
417
|
+
@mutex.synchronize { @waiting.delete pid }
|
|
418
|
+
exit_if_finished
|
|
419
|
+
end
|
|
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
|
+
}
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
private
|
|
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
|
+
|
|
445
|
+
def active_record_configured?
|
|
446
|
+
defined?(ActiveRecord::Base) && ActiveRecord::Base.configurations.any?
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
module Spring
|
|
2
|
+
class ApplicationManager
|
|
3
|
+
attr_reader :pid, :child, :app_env, :spawn_env, :spring_env, :status
|
|
4
|
+
|
|
5
|
+
def initialize(app_env, spawn_env, spring_env)
|
|
6
|
+
@app_env = app_env
|
|
7
|
+
@spawn_env = spawn_env
|
|
8
|
+
@spring_env = spring_env
|
|
9
|
+
@mutex = Mutex.new
|
|
10
|
+
@state = :running
|
|
11
|
+
@pid = nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def log(message)
|
|
15
|
+
spring_env.log "[application_manager:#{app_env}] #{message}"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# We're not using @mutex.synchronize to avoid the weird "<internal:prelude>:10"
|
|
19
|
+
# line which messes with backtraces in e.g. rspec
|
|
20
|
+
def synchronize
|
|
21
|
+
@mutex.lock
|
|
22
|
+
yield
|
|
23
|
+
ensure
|
|
24
|
+
@mutex.unlock
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def start
|
|
28
|
+
start_child
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def restart
|
|
32
|
+
return if @state == :stopping
|
|
33
|
+
start_child(true)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def alive?
|
|
37
|
+
@pid
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def with_child
|
|
41
|
+
synchronize do
|
|
42
|
+
if alive?
|
|
43
|
+
begin
|
|
44
|
+
yield
|
|
45
|
+
rescue Errno::ECONNRESET, Errno::EPIPE, Errno::EINVAL
|
|
46
|
+
# The child has died but has not been collected by the wait thread yet,
|
|
47
|
+
# so start a new child and try again.
|
|
48
|
+
log "child dead; starting"
|
|
49
|
+
start
|
|
50
|
+
yield
|
|
51
|
+
end
|
|
52
|
+
else
|
|
53
|
+
log "child not running; starting"
|
|
54
|
+
start
|
|
55
|
+
yield
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Returns the pid of the process running the command, or nil if the application process died.
|
|
61
|
+
def run(client)
|
|
62
|
+
with_child do
|
|
63
|
+
child.send_io client
|
|
64
|
+
child.gets or raise Errno::EPIPE
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
pid = child.gets.to_i
|
|
68
|
+
|
|
69
|
+
unless pid.zero?
|
|
70
|
+
log "got worker pid #{pid}"
|
|
71
|
+
pid
|
|
72
|
+
end
|
|
73
|
+
rescue Errno::ECONNRESET, Errno::EPIPE => e
|
|
74
|
+
log "#{e} while reading from child; returning no pid"
|
|
75
|
+
nil
|
|
76
|
+
ensure
|
|
77
|
+
client.close
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def stop
|
|
81
|
+
log "stopping"
|
|
82
|
+
@state = :stopping
|
|
83
|
+
|
|
84
|
+
if pid
|
|
85
|
+
Process.kill('TERM', pid)
|
|
86
|
+
Process.wait(pid)
|
|
87
|
+
end
|
|
88
|
+
rescue Errno::ESRCH, Errno::ECHILD
|
|
89
|
+
# Don't care
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def start_child(preload = false)
|
|
95
|
+
@child, child_socket = UNIXSocket.pair
|
|
96
|
+
|
|
97
|
+
Bundler.with_original_env do
|
|
98
|
+
bundler_dir = File.expand_path("../..", $LOADED_FEATURES.grep(/bundler\/setup\.rb$/).first)
|
|
99
|
+
@pid = Process.spawn(
|
|
100
|
+
{
|
|
101
|
+
"RAILS_ENV" => app_env,
|
|
102
|
+
"RACK_ENV" => app_env,
|
|
103
|
+
"SPRING_ORIGINAL_ENV" => JSON.dump(Spring::ORIGINAL_ENV),
|
|
104
|
+
"SPRING_PRELOAD" => preload ? "1" : "0",
|
|
105
|
+
"SPRING_SPAWN_ENV" => JSON.dump(spawn_env.compact),
|
|
106
|
+
**spawn_env,
|
|
107
|
+
},
|
|
108
|
+
"ruby",
|
|
109
|
+
*(bundler_dir != RbConfig::CONFIG["rubylibdir"] ? ["-I", bundler_dir] : []),
|
|
110
|
+
"-I", File.expand_path("../..", __FILE__),
|
|
111
|
+
"-e", "require 'spring/application/boot'",
|
|
112
|
+
3 => child_socket,
|
|
113
|
+
4 => spring_env.log_file,
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
start_wait_thread(pid, child) if child.gets
|
|
118
|
+
child_socket.close
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def start_wait_thread(pid, child)
|
|
122
|
+
Process.detach(pid)
|
|
123
|
+
|
|
124
|
+
Spring.failsafe_thread {
|
|
125
|
+
# The recv can raise an ECONNRESET, killing the thread, but that's ok
|
|
126
|
+
# as if it does we're no longer interested in the child
|
|
127
|
+
loop do
|
|
128
|
+
IO.select([child])
|
|
129
|
+
peek = child.recv(1, Socket::MSG_PEEK)
|
|
130
|
+
break if peek.nil? || peek.empty?
|
|
131
|
+
sleep 0.01
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
log "child #{pid} shutdown"
|
|
135
|
+
|
|
136
|
+
synchronize {
|
|
137
|
+
if @pid == pid
|
|
138
|
+
@pid = nil
|
|
139
|
+
restart
|
|
140
|
+
end
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
command = File.basename($0)
|
|
2
|
+
bin_path = File.expand_path("../../../bin/spring", __FILE__)
|
|
3
|
+
|
|
4
|
+
if command == "spring"
|
|
5
|
+
load bin_path
|
|
6
|
+
else
|
|
7
|
+
disable = ENV["DISABLE_SPRING"]
|
|
8
|
+
|
|
9
|
+
if Process.respond_to?(:fork) && (disable.nil? || disable.empty? || disable == "0")
|
|
10
|
+
ARGV.unshift(command)
|
|
11
|
+
load bin_path
|
|
12
|
+
end
|
|
13
|
+
end
|