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.
@@ -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