nrispring 2.1.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/LICENSE.txt +22 -0
- data/README.md +419 -0
- data/bin/spring +49 -0
- data/lib/spring/application.rb +384 -0
- data/lib/spring/application/boot.rb +19 -0
- data/lib/spring/application_manager.rb +141 -0
- data/lib/spring/binstub.rb +13 -0
- data/lib/spring/boot.rb +10 -0
- data/lib/spring/client.rb +48 -0
- data/lib/spring/client/binstub.rb +197 -0
- data/lib/spring/client/command.rb +18 -0
- data/lib/spring/client/help.rb +62 -0
- data/lib/spring/client/rails.rb +34 -0
- data/lib/spring/client/run.rb +232 -0
- data/lib/spring/client/server.rb +18 -0
- data/lib/spring/client/status.rb +30 -0
- data/lib/spring/client/stop.rb +22 -0
- data/lib/spring/client/version.rb +11 -0
- data/lib/spring/command_wrapper.rb +82 -0
- data/lib/spring/commands.rb +50 -0
- data/lib/spring/commands/rails.rb +112 -0
- data/lib/spring/commands/rake.rb +30 -0
- data/lib/spring/configuration.rb +58 -0
- data/lib/spring/env.rb +116 -0
- data/lib/spring/errors.rb +36 -0
- data/lib/spring/failsafe_thread.rb +14 -0
- data/lib/spring/json.rb +626 -0
- data/lib/spring/process_title_updater.rb +65 -0
- data/lib/spring/server.rb +150 -0
- data/lib/spring/sid.rb +42 -0
- data/lib/spring/version.rb +3 -0
- data/lib/spring/watcher.rb +30 -0
- data/lib/spring/watcher/abstract.rb +117 -0
- data/lib/spring/watcher/polling.rb +98 -0
- metadata +121 -0
@@ -0,0 +1,384 @@
|
|
1
|
+
require "spring/boot"
|
2
|
+
require "set"
|
3
|
+
require "pty"
|
4
|
+
|
5
|
+
module Spring
|
6
|
+
class Application
|
7
|
+
attr_reader :manager, :watcher, :spring_env, :original_env
|
8
|
+
|
9
|
+
def initialize(manager, original_env, spring_env = Env.new)
|
10
|
+
@manager = manager
|
11
|
+
@original_env = original_env
|
12
|
+
@spring_env = spring_env
|
13
|
+
@mutex = Mutex.new
|
14
|
+
@waiting = Set.new
|
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 app_env
|
32
|
+
ENV['RAILS_ENV']
|
33
|
+
end
|
34
|
+
|
35
|
+
def app_name
|
36
|
+
spring_env.app_name
|
37
|
+
end
|
38
|
+
|
39
|
+
def log(message)
|
40
|
+
spring_env.log "[application:#{app_env}] #{message}"
|
41
|
+
end
|
42
|
+
|
43
|
+
def preloaded?
|
44
|
+
@preloaded
|
45
|
+
end
|
46
|
+
|
47
|
+
def preload_failed?
|
48
|
+
@preloaded == :failure
|
49
|
+
end
|
50
|
+
|
51
|
+
def exiting?
|
52
|
+
@state == :exiting
|
53
|
+
end
|
54
|
+
|
55
|
+
def terminating?
|
56
|
+
@state == :terminating
|
57
|
+
end
|
58
|
+
|
59
|
+
def watcher_stale?
|
60
|
+
@state == :watcher_stale
|
61
|
+
end
|
62
|
+
|
63
|
+
def initialized?
|
64
|
+
@state == :initialized
|
65
|
+
end
|
66
|
+
|
67
|
+
def start_watcher
|
68
|
+
@watcher = Spring.watcher
|
69
|
+
|
70
|
+
@watcher.on_stale do
|
71
|
+
state! :watcher_stale
|
72
|
+
end
|
73
|
+
|
74
|
+
if @watcher.respond_to? :on_debug
|
75
|
+
@watcher.on_debug do |message|
|
76
|
+
spring_env.log "[watcher:#{app_env}] #{message}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
@watcher.start
|
81
|
+
end
|
82
|
+
|
83
|
+
def preload
|
84
|
+
log "preloading app"
|
85
|
+
|
86
|
+
begin
|
87
|
+
require "spring/commands"
|
88
|
+
ensure
|
89
|
+
start_watcher
|
90
|
+
end
|
91
|
+
|
92
|
+
require Spring.application_root_path.join("config", "application")
|
93
|
+
|
94
|
+
unless Rails.respond_to?(:gem_version) && Rails.gem_version >= Gem::Version.new('4.2.0')
|
95
|
+
raise "Spring only supports Rails >= 4.2.0"
|
96
|
+
end
|
97
|
+
|
98
|
+
# config/environments/test.rb will have config.cache_classes = true. However
|
99
|
+
# we want it to be false so that we can reload files. This is a hack to
|
100
|
+
# override the effect of config.cache_classes = true. We can then actually
|
101
|
+
# set config.cache_classes = false after loading the environment.
|
102
|
+
Rails::Application.initializer :initialize_dependency_mechanism, group: :all do
|
103
|
+
ActiveSupport::Dependencies.mechanism = :load
|
104
|
+
end
|
105
|
+
|
106
|
+
require Spring.application_root_path.join("config", "environment")
|
107
|
+
|
108
|
+
@original_cache_classes = Rails.application.config.cache_classes
|
109
|
+
Rails.application.config.cache_classes = false
|
110
|
+
|
111
|
+
disconnect_database
|
112
|
+
|
113
|
+
@preloaded = :success
|
114
|
+
rescue Exception => e
|
115
|
+
@preloaded = :failure
|
116
|
+
watcher.add e.backtrace.map { |line| line[/^(.*)\:\d+/, 1] }
|
117
|
+
raise e unless initialized?
|
118
|
+
ensure
|
119
|
+
watcher.add loaded_application_features
|
120
|
+
watcher.add Spring.gemfile, "#{Spring.gemfile}.lock"
|
121
|
+
|
122
|
+
if defined?(Rails) && Rails.application
|
123
|
+
watcher.add Rails.application.paths["config/initializers"]
|
124
|
+
watcher.add Rails.application.paths["config/database"]
|
125
|
+
if secrets_path = Rails.application.paths["config/secrets"]
|
126
|
+
watcher.add secrets_path
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def eager_preload
|
132
|
+
with_pty do
|
133
|
+
# we can't see stderr and there could be issues when it's overflown
|
134
|
+
# see https://github.com/rails/spring/issues/396
|
135
|
+
STDERR.reopen("/dev/null")
|
136
|
+
preload
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def run
|
141
|
+
state :running
|
142
|
+
manager.puts
|
143
|
+
|
144
|
+
loop do
|
145
|
+
IO.select [manager, @interrupt.first]
|
146
|
+
|
147
|
+
if terminating? || watcher_stale? || preload_failed?
|
148
|
+
exit
|
149
|
+
else
|
150
|
+
serve manager.recv_io(UNIXSocket)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def serve(client)
|
156
|
+
log "got client"
|
157
|
+
manager.puts
|
158
|
+
|
159
|
+
_stdout, stderr, _stdin = streams = 3.times.map { client.recv_io }
|
160
|
+
[STDOUT, STDERR, STDIN].zip(streams).each { |a, b| a.reopen(b) }
|
161
|
+
|
162
|
+
preload unless preloaded?
|
163
|
+
|
164
|
+
args, env = JSON.load(client.read(client.gets.to_i)).values_at("args", "env")
|
165
|
+
command = Spring.command(args.shift)
|
166
|
+
|
167
|
+
connect_database
|
168
|
+
setup command
|
169
|
+
|
170
|
+
if Rails.application.reloaders.any?(&:updated?)
|
171
|
+
# Rails 5.1 forward-compat. AD::R is deprecated to AS::R in Rails 5.
|
172
|
+
if defined? ActiveSupport::Reloader
|
173
|
+
Rails.application.reloader.reload!
|
174
|
+
else
|
175
|
+
ActionDispatch::Reloader.cleanup!
|
176
|
+
ActionDispatch::Reloader.prepare!
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# Ensure we boot the process in the directory the command was called from,
|
181
|
+
# not from the directory Spring started in
|
182
|
+
original_dir = Dir.pwd
|
183
|
+
Dir.chdir(env['PWD'] || original_dir)
|
184
|
+
|
185
|
+
pid = fork {
|
186
|
+
Process.setsid
|
187
|
+
IGNORE_SIGNALS.each { |sig| trap(sig, "DEFAULT") }
|
188
|
+
trap("TERM", "DEFAULT")
|
189
|
+
|
190
|
+
unless Spring.quiet
|
191
|
+
STDERR.puts "Running via Spring preloader in process #{Process.pid}"
|
192
|
+
|
193
|
+
if Rails.env.production?
|
194
|
+
STDERR.puts "WARNING: Spring is running in production. To fix " \
|
195
|
+
"this make sure the spring gem is only present " \
|
196
|
+
"in `development` and `test` groups in your Gemfile " \
|
197
|
+
"and make sure you always use " \
|
198
|
+
"`bundle install --without development test` in production"
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
ARGV.replace(args)
|
203
|
+
$0 = command.exec_name
|
204
|
+
|
205
|
+
# Delete all env vars which are unchanged from before Spring started
|
206
|
+
original_env.each { |k, v| ENV.delete k if ENV[k] == v }
|
207
|
+
|
208
|
+
# Load in the current env vars, except those which *were* changed when Spring started
|
209
|
+
env.each { |k, v| ENV[k] ||= v }
|
210
|
+
|
211
|
+
# requiring is faster, so if config.cache_classes was true in
|
212
|
+
# the environment's config file, then we can respect that from
|
213
|
+
# here on as we no longer need constant reloading.
|
214
|
+
if @original_cache_classes
|
215
|
+
ActiveSupport::Dependencies.mechanism = :require
|
216
|
+
Rails.application.config.cache_classes = true
|
217
|
+
end
|
218
|
+
|
219
|
+
connect_database
|
220
|
+
srand
|
221
|
+
|
222
|
+
invoke_after_fork_callbacks
|
223
|
+
shush_backtraces
|
224
|
+
|
225
|
+
command.call
|
226
|
+
}
|
227
|
+
|
228
|
+
disconnect_database
|
229
|
+
|
230
|
+
log "forked #{pid}"
|
231
|
+
manager.puts pid
|
232
|
+
|
233
|
+
wait pid, streams, client
|
234
|
+
rescue Exception => e
|
235
|
+
log "exception: #{e}"
|
236
|
+
manager.puts unless pid
|
237
|
+
|
238
|
+
if streams && !e.is_a?(SystemExit)
|
239
|
+
print_exception(stderr, e)
|
240
|
+
streams.each(&:close)
|
241
|
+
end
|
242
|
+
|
243
|
+
client.puts(1) if pid
|
244
|
+
client.close
|
245
|
+
ensure
|
246
|
+
# Redirect STDOUT and STDERR to prevent from keeping the original FDs
|
247
|
+
# (i.e. to prevent `spring rake -T | grep db` from hanging forever),
|
248
|
+
# even when exception is raised before forking (i.e. preloading).
|
249
|
+
reset_streams
|
250
|
+
Dir.chdir(original_dir)
|
251
|
+
end
|
252
|
+
|
253
|
+
def terminate
|
254
|
+
if exiting?
|
255
|
+
# Ensure that we do not ignore subsequent termination attempts
|
256
|
+
log "forced exit"
|
257
|
+
@waiting.each { |pid| Process.kill("TERM", pid) }
|
258
|
+
Kernel.exit
|
259
|
+
else
|
260
|
+
state! :terminating
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
def exit
|
265
|
+
state :exiting
|
266
|
+
manager.shutdown(:RDWR)
|
267
|
+
exit_if_finished
|
268
|
+
sleep
|
269
|
+
end
|
270
|
+
|
271
|
+
def exit_if_finished
|
272
|
+
@mutex.synchronize {
|
273
|
+
Kernel.exit if exiting? && @waiting.empty?
|
274
|
+
}
|
275
|
+
end
|
276
|
+
|
277
|
+
# The command might need to require some files in the
|
278
|
+
# main process so that they are cached. For example a test command wants to
|
279
|
+
# load the helper file once and have it cached.
|
280
|
+
def setup(command)
|
281
|
+
if command.setup
|
282
|
+
watcher.add loaded_application_features # loaded features may have changed
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
def invoke_after_fork_callbacks
|
287
|
+
Spring.after_fork_callbacks.each do |callback|
|
288
|
+
callback.call
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
def loaded_application_features
|
293
|
+
root = Spring.application_root_path.to_s
|
294
|
+
$LOADED_FEATURES.select { |f| f.start_with?(root) }
|
295
|
+
end
|
296
|
+
|
297
|
+
def disconnect_database
|
298
|
+
ActiveRecord::Base.remove_connection if active_record_configured?
|
299
|
+
end
|
300
|
+
|
301
|
+
def connect_database
|
302
|
+
ActiveRecord::Base.establish_connection if active_record_configured?
|
303
|
+
end
|
304
|
+
|
305
|
+
# This feels very naughty
|
306
|
+
def shush_backtraces
|
307
|
+
Kernel.module_eval do
|
308
|
+
old_raise = Kernel.method(:raise)
|
309
|
+
remove_method :raise
|
310
|
+
define_method :raise do |*args|
|
311
|
+
begin
|
312
|
+
old_raise.call(*args)
|
313
|
+
ensure
|
314
|
+
if $!
|
315
|
+
lib = File.expand_path("..", __FILE__)
|
316
|
+
$!.backtrace.reject! { |line| line.start_with?(lib) }
|
317
|
+
end
|
318
|
+
end
|
319
|
+
end
|
320
|
+
private :raise
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
def print_exception(stream, error)
|
325
|
+
first, rest = error.backtrace.first, error.backtrace.drop(1)
|
326
|
+
stream.puts("#{first}: #{error} (#{error.class})")
|
327
|
+
rest.each { |line| stream.puts("\tfrom #{line}") }
|
328
|
+
end
|
329
|
+
|
330
|
+
def with_pty
|
331
|
+
PTY.open do |master, slave|
|
332
|
+
[STDOUT, STDERR, STDIN].each { |s| s.reopen slave }
|
333
|
+
reader_thread = Spring.failsafe_thread { master.read }
|
334
|
+
begin
|
335
|
+
yield
|
336
|
+
ensure
|
337
|
+
reader_thread.kill
|
338
|
+
reset_streams
|
339
|
+
end
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
def reset_streams
|
344
|
+
[STDOUT, STDERR].each { |stream| stream.reopen(spring_env.log_file) }
|
345
|
+
STDIN.reopen("/dev/null")
|
346
|
+
end
|
347
|
+
|
348
|
+
def wait(pid, streams, client)
|
349
|
+
@mutex.synchronize { @waiting << pid }
|
350
|
+
|
351
|
+
# Wait in a separate thread so we can run multiple commands at once
|
352
|
+
Spring.failsafe_thread {
|
353
|
+
begin
|
354
|
+
_, status = Process.wait2 pid
|
355
|
+
log "#{pid} exited with #{status.exitstatus}"
|
356
|
+
|
357
|
+
streams.each(&:close)
|
358
|
+
client.puts(status.exitstatus)
|
359
|
+
client.close
|
360
|
+
ensure
|
361
|
+
@mutex.synchronize { @waiting.delete pid }
|
362
|
+
exit_if_finished
|
363
|
+
end
|
364
|
+
}
|
365
|
+
|
366
|
+
Spring.failsafe_thread {
|
367
|
+
while signal = client.gets.chomp
|
368
|
+
begin
|
369
|
+
Process.kill(signal, -Process.getpgid(pid))
|
370
|
+
client.puts(0)
|
371
|
+
rescue Errno::ESRCH
|
372
|
+
client.puts(1)
|
373
|
+
end
|
374
|
+
end
|
375
|
+
}
|
376
|
+
end
|
377
|
+
|
378
|
+
private
|
379
|
+
|
380
|
+
def active_record_configured?
|
381
|
+
defined?(ActiveRecord::Base) && ActiveRecord::Base.configurations.any?
|
382
|
+
end
|
383
|
+
end
|
384
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# This is necessary for the terminal to work correctly when we reopen stdin.
|
2
|
+
Process.setsid
|
3
|
+
|
4
|
+
require "spring/application"
|
5
|
+
|
6
|
+
app = Spring::Application.new(
|
7
|
+
UNIXSocket.for_fd(3),
|
8
|
+
Spring::JSON.load(ENV.delete("SPRING_ORIGINAL_ENV").dup),
|
9
|
+
Spring::Env.new(log_file: IO.for_fd(4))
|
10
|
+
)
|
11
|
+
|
12
|
+
Signal.trap("TERM") { app.terminate }
|
13
|
+
|
14
|
+
Spring::ProcessTitleUpdater.run { |distance|
|
15
|
+
"spring app | #{app.app_name} | started #{distance} ago | #{app.app_env} mode"
|
16
|
+
}
|
17
|
+
|
18
|
+
app.eager_preload if ENV.delete("SPRING_PRELOAD") == "1"
|
19
|
+
app.run
|
@@ -0,0 +1,141 @@
|
|
1
|
+
module Spring
|
2
|
+
class ApplicationManager
|
3
|
+
attr_reader :pid, :child, :app_env, :spring_env, :status
|
4
|
+
|
5
|
+
def initialize(app_env, spring_env)
|
6
|
+
@app_env = app_env
|
7
|
+
@spring_env = spring_env
|
8
|
+
@mutex = Mutex.new
|
9
|
+
@state = :running
|
10
|
+
@pid = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def log(message)
|
14
|
+
spring_env.log "[application_manager:#{app_env}] #{message}"
|
15
|
+
end
|
16
|
+
|
17
|
+
# We're not using @mutex.synchronize to avoid the weird "<internal:prelude>:10"
|
18
|
+
# line which messes with backtraces in e.g. rspec
|
19
|
+
def synchronize
|
20
|
+
@mutex.lock
|
21
|
+
yield
|
22
|
+
ensure
|
23
|
+
@mutex.unlock
|
24
|
+
end
|
25
|
+
|
26
|
+
def start
|
27
|
+
start_child
|
28
|
+
end
|
29
|
+
|
30
|
+
def restart
|
31
|
+
return if @state == :stopping
|
32
|
+
start_child(true)
|
33
|
+
end
|
34
|
+
|
35
|
+
def alive?
|
36
|
+
@pid
|
37
|
+
end
|
38
|
+
|
39
|
+
def with_child
|
40
|
+
synchronize do
|
41
|
+
if alive?
|
42
|
+
begin
|
43
|
+
yield
|
44
|
+
rescue Errno::ECONNRESET, Errno::EPIPE
|
45
|
+
# The child has died but has not been collected by the wait thread yet,
|
46
|
+
# so start a new child and try again.
|
47
|
+
log "child dead; starting"
|
48
|
+
start
|
49
|
+
yield
|
50
|
+
end
|
51
|
+
else
|
52
|
+
log "child not running; starting"
|
53
|
+
start
|
54
|
+
yield
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns the pid of the process running the command, or nil if the application process died.
|
60
|
+
def run(client)
|
61
|
+
with_child do
|
62
|
+
child.send_io client
|
63
|
+
child.gets or raise Errno::EPIPE
|
64
|
+
end
|
65
|
+
|
66
|
+
pid = child.gets.to_i
|
67
|
+
|
68
|
+
unless pid.zero?
|
69
|
+
log "got worker pid #{pid}"
|
70
|
+
pid
|
71
|
+
end
|
72
|
+
rescue Errno::ECONNRESET, Errno::EPIPE => e
|
73
|
+
log "#{e} while reading from child; returning no pid"
|
74
|
+
nil
|
75
|
+
ensure
|
76
|
+
client.close
|
77
|
+
end
|
78
|
+
|
79
|
+
def stop
|
80
|
+
log "stopping"
|
81
|
+
@state = :stopping
|
82
|
+
|
83
|
+
if pid
|
84
|
+
Process.kill('TERM', pid)
|
85
|
+
Process.wait(pid)
|
86
|
+
end
|
87
|
+
rescue Errno::ESRCH, Errno::ECHILD
|
88
|
+
# Don't care
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def start_child(preload = false)
|
94
|
+
@child, child_socket = UNIXSocket.pair
|
95
|
+
|
96
|
+
Bundler.with_original_env do
|
97
|
+
bundler_dir = File.expand_path("../..", $LOADED_FEATURES.grep(/bundler\/setup\.rb$/).first)
|
98
|
+
@pid = Process.spawn(
|
99
|
+
{
|
100
|
+
"RAILS_ENV" => app_env,
|
101
|
+
"RACK_ENV" => app_env,
|
102
|
+
"SPRING_ORIGINAL_ENV" => JSON.dump(Spring::ORIGINAL_ENV),
|
103
|
+
"SPRING_PRELOAD" => preload ? "1" : "0"
|
104
|
+
},
|
105
|
+
"ruby",
|
106
|
+
*(bundler_dir != RbConfig::CONFIG["rubylibdir"] ? ["-I", bundler_dir] : []),
|
107
|
+
"-I", File.expand_path("../..", __FILE__),
|
108
|
+
"-e", "require 'spring/application/boot'",
|
109
|
+
3 => child_socket,
|
110
|
+
4 => spring_env.log_file,
|
111
|
+
)
|
112
|
+
end
|
113
|
+
|
114
|
+
start_wait_thread(pid, child) if child.gets
|
115
|
+
child_socket.close
|
116
|
+
end
|
117
|
+
|
118
|
+
def start_wait_thread(pid, child)
|
119
|
+
Process.detach(pid)
|
120
|
+
|
121
|
+
Spring.failsafe_thread {
|
122
|
+
# The recv can raise an ECONNRESET, killing the thread, but that's ok
|
123
|
+
# as if it does we're no longer interested in the child
|
124
|
+
loop do
|
125
|
+
IO.select([child])
|
126
|
+
break if child.recv(1, Socket::MSG_PEEK).empty?
|
127
|
+
sleep 0.01
|
128
|
+
end
|
129
|
+
|
130
|
+
log "child #{pid} shutdown"
|
131
|
+
|
132
|
+
synchronize {
|
133
|
+
if @pid == pid
|
134
|
+
@pid = nil
|
135
|
+
restart
|
136
|
+
end
|
137
|
+
}
|
138
|
+
}
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|