|
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