|
2 | 2 | require "set"
|
3 | 3 | require "pty"
|
4 | 4 | require "spring/platform"
|
| 5 | +require "spring/application/base" |
| 6 | +require "spring/application/pool_strategy" |
| 7 | +require "spring/application/fork_strategy" |
5 | 8 |
|
6 | 9 | module Spring
|
7 |
| - class Application |
8 |
| - if Spring.fork? |
9 |
| - require 'spring/application/fork_strategy' |
10 |
| - include ForkStrategy |
11 |
| - else |
12 |
| - require 'spring/application/pool_strategy' |
13 |
| - include PoolStrategy |
14 |
| - end |
15 |
| - attr_reader :manager, :watcher, :spring_env, :original_env |
16 |
| - |
17 |
| - def initialize(manager, original_env) |
18 |
| - @manager = manager |
19 |
| - @original_env = original_env |
20 |
| - @spring_env = Env.new |
21 |
| - @mutex = Mutex.new |
22 |
| - @waiting = Set.new |
23 |
| - @preloaded = false |
24 |
| - @state = :initialized |
25 |
| - @interrupt = IO.pipe |
26 |
| - end |
27 |
| - |
28 |
| - def state(val) |
29 |
| - return if exiting? |
30 |
| - log "#{@state} -> #{val}" |
31 |
| - @state = val |
32 |
| - end |
33 |
| - |
34 |
| - def state!(val) |
35 |
| - state val |
36 |
| - @interrupt.last.write "." |
37 |
| - end |
38 |
| - |
39 |
| - def app_env |
40 |
| - ENV['RAILS_ENV'] |
41 |
| - end |
42 |
| - |
43 |
| - def app_name |
44 |
| - spring_env.app_name |
45 |
| - end |
46 |
| - |
47 |
| - def log(message) |
48 |
| - spring_env.log "[application:#{app_env}] #{message}" |
49 |
| - end |
50 |
| - |
51 |
| - def preloaded? |
52 |
| - @preloaded |
53 |
| - end |
54 |
| - |
55 |
| - def preload_failed? |
56 |
| - @preloaded == :failure |
57 |
| - end |
58 |
| - |
59 |
| - def exiting? |
60 |
| - @state == :exiting |
61 |
| - end |
62 |
| - |
63 |
| - def terminating? |
64 |
| - @state == :terminating |
65 |
| - end |
66 |
| - |
67 |
| - def watcher_stale? |
68 |
| - @state == :watcher_stale |
69 |
| - end |
70 |
| - |
71 |
| - def initialized? |
72 |
| - @state == :initialized |
73 |
| - end |
74 |
| - |
75 |
| - def start_watcher |
76 |
| - @watcher = Spring.watcher |
77 |
| - @watcher.on_stale { state! :watcher_stale } |
78 |
| - @watcher.start |
79 |
| - end |
80 |
| - |
81 |
| - def preload |
82 |
| - log "preloading app" |
83 |
| - |
84 |
| - begin |
85 |
| - require "spring/commands" |
86 |
| - ensure |
87 |
| - start_watcher |
88 |
| - end |
89 |
| - |
90 |
| - require Spring.application_root_path.join("config", "application") |
91 |
| - |
92 |
| - # config/environments/test.rb will have config.cache_classes = true. However |
93 |
| - # we want it to be false so that we can reload files. This is a hack to |
94 |
| - # override the effect of config.cache_classes = true. We can then actually |
95 |
| - # set config.cache_classes = false after loading the environment. |
96 |
| - Rails::Application.initializer :initialize_dependency_mechanism, group: :all do |
97 |
| - ActiveSupport::Dependencies.mechanism = :load |
98 |
| - end |
99 |
| - |
100 |
| - require Spring.application_root_path.join("config", "environment") |
101 |
| - |
102 |
| - @original_cache_classes = Rails.application.config.cache_classes |
103 |
| - Rails.application.config.cache_classes = false |
104 |
| - |
105 |
| - disconnect_database |
106 |
| - |
107 |
| - @preloaded = :success |
108 |
| - rescue Exception => e |
109 |
| - @preloaded = :failure |
110 |
| - watcher.add e.backtrace.map { |line| line[/^(.*)\:\d+/, 1] } |
111 |
| - raise e unless initialized? |
112 |
| - ensure |
113 |
| - watcher.add loaded_application_features |
114 |
| - watcher.add Spring.gemfile, "#{Spring.gemfile}.lock" |
115 |
| - |
116 |
| - if defined?(Rails) && Rails.application |
117 |
| - watcher.add Rails.application.paths["config/initializers"] |
118 |
| - watcher.add Rails.application.paths["config/database"] |
119 |
| - if secrets_path = Rails.application.paths["config/secrets"] |
120 |
| - watcher.add secrets_path |
121 |
| - end |
122 |
| - end |
123 |
| - end |
124 |
| - |
125 |
| - def run |
126 |
| - state :running |
127 |
| - manager.puts Process.pid |
128 |
| - |
129 |
| - loop do |
130 |
| - IO.select [manager, @interrupt.first] |
131 |
| - |
132 |
| - if terminating? || watcher_stale? || preload_failed? |
133 |
| - exit |
134 |
| - else |
135 |
| - serve manager.recv_io(UNIXSocket) |
136 |
| - end |
137 |
| - end |
138 |
| - end |
139 |
| - |
140 |
| - def serve(client) |
141 |
| - app_started = [false] |
142 |
| - log "got client" |
143 |
| - manager.puts |
144 |
| - |
145 |
| - stdout, stderr, stdin = streams = 3.times.map { client.recv_io } |
146 |
| - [STDOUT, STDERR, STDIN].zip(streams).each { |a, b| a.reopen(b) } |
147 |
| - |
148 |
| - preload unless preloaded? |
149 |
| - |
150 |
| - args, env = JSON.load(client.read(client.gets.to_i)).values_at("args", "env") |
151 |
| - command = Spring.command(args.shift) |
152 |
| - |
153 |
| - connect_database |
154 |
| - setup command |
155 |
| - |
156 |
| - if Rails.application.reloaders.any?(&:updated?) |
157 |
| - ActionDispatch::Reloader.cleanup! |
158 |
| - ActionDispatch::Reloader.prepare! |
159 |
| - end |
160 |
| - |
161 |
| - start_app(client, streams, app_started) { |
162 |
| - IGNORE_SIGNALS.each { |sig| trap(sig, "DEFAULT") } |
163 |
| - trap("TERM", "DEFAULT") |
164 |
| - |
165 |
| - STDERR.puts "Running via Spring preloader in process #{Process.pid}" unless Spring.quiet |
166 |
| - |
167 |
| - ARGV.replace(args) |
168 |
| - $0 = command.exec_name |
169 |
| - |
170 |
| - # Delete all env vars which are unchanged from before spring started |
171 |
| - original_env.each { |k, v| ENV.delete k if ENV[k] == v } |
172 |
| - |
173 |
| - # Load in the current env vars, except those which *were* changed when spring started |
174 |
| - env.each { |k, v| ENV[k] ||= v } |
175 |
| - |
176 |
| - # requiring is faster, so if config.cache_classes was true in |
177 |
| - # the environment's config file, then we can respect that from |
178 |
| - # here on as we no longer need constant reloading. |
179 |
| - if @original_cache_classes |
180 |
| - ActiveSupport::Dependencies.mechanism = :require |
181 |
| - Rails.application.config.cache_classes = true |
182 |
| - end |
183 |
| - |
184 |
| - connect_database |
185 |
| - srand |
186 |
| - |
187 |
| - invoke_after_fork_callbacks |
188 |
| - shush_backtraces |
189 |
| - |
190 |
| - command.call |
191 |
| - } |
192 |
| - rescue Exception => e |
193 |
| - Kernel.exit if exiting? && e.is_a?(SystemExit) |
194 |
| - |
195 |
| - log "exception: #{e}" |
196 |
| - manager.puts unless app_started[0] |
197 |
| - |
198 |
| - if streams && !e.is_a?(SystemExit) |
199 |
| - print_exception(stderr, e) |
200 |
| - streams.each(&:close) |
201 |
| - end |
202 |
| - |
203 |
| - client.puts(1) if app_started[0] |
204 |
| - client.close |
205 |
| - end |
206 |
| - |
207 |
| - def terminate |
208 |
| - if exiting? |
209 |
| - # Ensure that we do not ignore subsequent termination attempts |
210 |
| - log "forced exit" |
211 |
| - @waiting.each { |pid| Process.kill("TERM", pid) } |
212 |
| - Kernel.exit |
213 |
| - else |
214 |
| - state! :terminating |
215 |
| - end |
216 |
| - end |
217 |
| - |
218 |
| - def exit |
219 |
| - state :exiting |
220 |
| - manager.shutdown(:RDWR) |
221 |
| - exit_if_finished |
222 |
| - sleep |
223 |
| - end |
224 |
| - |
225 |
| - def exit_if_finished |
226 |
| - @mutex.synchronize { |
227 |
| - Kernel.exit if exiting? && @waiting.empty? |
228 |
| - } |
229 |
| - end |
230 |
| - |
231 |
| - # The command might need to require some files in the |
232 |
| - # main process so that they are cached. For example a test command wants to |
233 |
| - # load the helper file once and have it cached. |
234 |
| - def setup(command) |
235 |
| - if command.setup |
236 |
| - watcher.add loaded_application_features # loaded features may have changed |
237 |
| - end |
238 |
| - end |
239 |
| - |
240 |
| - def invoke_after_fork_callbacks |
241 |
| - Spring.after_fork_callbacks.each do |callback| |
242 |
| - callback.call |
243 |
| - end |
244 |
| - end |
245 |
| - |
246 |
| - def loaded_application_features |
247 |
| - root = Spring.application_root_path.to_s |
248 |
| - $LOADED_FEATURES.select { |f| f.start_with?(root) } |
249 |
| - end |
250 |
| - |
251 |
| - def disconnect_database |
252 |
| - ActiveRecord::Base.remove_connection if active_record_configured? |
253 |
| - end |
254 |
| - |
255 |
| - def connect_database |
256 |
| - ActiveRecord::Base.establish_connection if active_record_configured? |
257 |
| - end |
258 |
| - |
259 |
| - # This feels very naughty |
260 |
| - def shush_backtraces |
261 |
| - Kernel.module_eval do |
262 |
| - old_raise = Kernel.method(:raise) |
263 |
| - remove_method :raise |
264 |
| - define_method :raise do |*args| |
265 |
| - begin |
266 |
| - old_raise.call(*args) |
267 |
| - ensure |
268 |
| - if $! |
269 |
| - lib = File.expand_path("..", __FILE__) |
270 |
| - $!.backtrace.reject! { |line| line.start_with?(lib) } |
271 |
| - end |
272 |
| - end |
273 |
| - end |
274 |
| - private :raise |
275 |
| - end |
276 |
| - end |
277 |
| - |
278 |
| - def print_exception(stream, error) |
279 |
| - first, rest = error.backtrace.first, error.backtrace.drop(1) |
280 |
| - stream.puts("#{first}: #{error} (#{error.class})") |
281 |
| - rest.each { |line| stream.puts("\tfrom #{line}") } |
282 |
| - end |
283 |
| - |
284 |
| - def reset_streams |
285 |
| - [STDOUT, STDERR].each { |stream| stream.reopen(spring_env.log_file) } |
286 |
| - STDIN.reopen("/dev/null") |
287 |
| - end |
288 |
| - |
289 |
| - private |
290 |
| - |
291 |
| - def active_record_configured? |
292 |
| - defined?(ActiveRecord::Base) && ActiveRecord::Base.configurations.any? |
| 10 | + module Application |
| 11 | + def self.create(*args) |
| 12 | + strategy = Spring.fork? ? ForkStrategy : PoolStrategy |
| 13 | + strategy.new(*args) |
293 | 14 | end
|
294 | 15 | end
|
295 | 16 | end
|
0 commit comments