nrispring 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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