File: application.rb

package info (click to toggle)
ruby-spring 1.1.3-1
  • links: PTS, VCS
  • area: main
  • in suites: jessie, jessie-kfreebsd
  • size: 364 kB
  • ctags: 429
  • sloc: ruby: 2,762; makefile: 6
file content (317 lines) | stat: -rw-r--r-- 7,910 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
require "spring/boot"
require "set"
require "pty"

module Spring
  class Application
    attr_reader :manager, :watcher, :spring_env, :original_env

    def initialize(manager, original_env)
      @manager      = manager
      @original_env = original_env
      @spring_env   = Env.new
      @mutex        = Mutex.new
      @waiting      = Set.new
      @preloaded    = false
      @state        = :initialized
      @interrupt    = IO.pipe
    end

    def state(val)
      return if exiting?
      log "#{@state} -> #{val}"
      @state = val
    end

    def state!(val)
      state val
      @interrupt.last.write "."
    end

    def app_env
      ENV['RAILS_ENV']
    end

    def app_name
      spring_env.app_name
    end

    def log(message)
      spring_env.log "[application:#{app_env}] #{message}"
    end

    def preloaded?
      @preloaded
    end

    def preload_failed?
      @preloaded == :failure
    end

    def exiting?
      @state == :exiting
    end

    def terminating?
      @state == :terminating
    end

    def watcher_stale?
      @state == :watcher_stale
    end

    def initialized?
      @state == :initialized
    end

    def start_watcher
      @watcher = Spring.watcher
      @watcher.on_stale { state! :watcher_stale }
      @watcher.start
    end

    def preload
      log "preloading app"

      begin
        require "spring/commands"
      ensure
        start_watcher
      end

      require Spring.application_root_path.join("config", "application")

      # config/environments/test.rb will have config.cache_classes = true. However
      # we want it to be false so that we can reload files. This is a hack to
      # override the effect of config.cache_classes = true. We can then actually
      # set config.cache_classes = false after loading the environment.
      Rails::Application.initializer :initialize_dependency_mechanism, group: :all do
        ActiveSupport::Dependencies.mechanism = :load
      end

      require Spring.application_root_path.join("config", "environment")

      @original_cache_classes = Rails.application.config.cache_classes
      Rails.application.config.cache_classes = false

      disconnect_database

      @preloaded = :success
    rescue Exception => e
      @preloaded = :failure
      watcher.add e.backtrace.map { |line| line.match(/^(.*)\:\d+\:in /)[1] }
      raise e unless initialized?
    ensure
      watcher.add loaded_application_features
      watcher.add Spring.gemfile, "#{Spring.gemfile}.lock"

      if defined?(Rails) && Rails.application
        watcher.add Rails.application.paths["config/initializers"]
        watcher.add Rails.application.paths["config/database"]
      end
    end

    def eager_preload
      with_pty { preload }
    end

    def run
      state :running
      manager.puts

      loop do
        IO.select [manager, @interrupt.first]

        if terminating? || watcher_stale? || preload_failed?
          exit
        else
          serve manager.recv_io(UNIXSocket)
        end
      end
    end

    def serve(client)
      log "got client"
      manager.puts

      stdout, stderr, stdin = streams = 3.times.map { client.recv_io }
      [STDOUT, STDERR, STDIN].zip(streams).each { |a, b| a.reopen(b) }

      preload unless preloaded?

      args, env = JSON.load(client.read(client.gets.to_i)).values_at("args", "env")
      command   = Spring.command(args.shift)

      connect_database
      setup command

      if Rails.application.reloaders.any?(&:updated?)
        ActionDispatch::Reloader.cleanup!
        ActionDispatch::Reloader.prepare!
      end

      pid = fork {
        IGNORE_SIGNALS.each { |sig| trap(sig, "DEFAULT") }
        trap("TERM", "DEFAULT")

        ARGV.replace(args)
        $0 = command.process_title

        # Delete all env vars which are unchanged from before spring started
        original_env.each { |k, v| ENV.delete k if ENV[k] == v }

        # Load in the current env vars, except those which *were* changed when spring started
        env.each { |k, v| ENV[k] ||= v }

        # requiring is faster, so if config.cache_classes was true in
        # the environment's config file, then we can respect that from
        # here on as we no longer need constant reloading.
        if @original_cache_classes
          ActiveSupport::Dependencies.mechanism = :require
          Rails.application.config.cache_classes = true
        end

        connect_database
        srand

        invoke_after_fork_callbacks
        shush_backtraces

        command.call
      }

      disconnect_database
      reset_streams

      log "forked #{pid}"
      manager.puts pid

      wait pid, streams, client
    rescue Exception => e
      log "exception: #{e}"
      manager.puts unless pid

      if streams && !e.is_a?(SystemExit)
        print_exception(stderr, e)
        streams.each(&:close)
      end

      client.puts(1) if pid
      client.close
    end

    def terminate
      if exiting?
        # Ensure that we do not ignore subsequent termination attempts
        log "forced exit"
        @waiting.each { |pid| Process.kill("TERM", pid) }
        Kernel.exit
      else
        state! :terminating
      end
    end

    def exit
      state :exiting
      manager.shutdown(:RDWR)
      exit_if_finished
      sleep
    end

    def exit_if_finished
      @mutex.synchronize {
        Kernel.exit if exiting? && @waiting.empty?
      }
    end

    # The command might need to require some files in the
    # main process so that they are cached. For example a test command wants to
    # load the helper file once and have it cached.
    def setup(command)
      if command.setup
        watcher.add loaded_application_features # loaded features may have changed
      end
    end

    def invoke_after_fork_callbacks
      Spring.after_fork_callbacks.each do |callback|
        callback.call
      end
    end

    def loaded_application_features
      root = Spring.application_root_path.to_s
      $LOADED_FEATURES.select { |f| f.start_with?(root) }
    end

    def disconnect_database
      ActiveRecord::Base.remove_connection if active_record_configured?
    end

    def connect_database
      ActiveRecord::Base.establish_connection if active_record_configured?
    end

    # This feels very naughty
    def shush_backtraces
      Kernel.module_eval do
        old_raise = Kernel.method(:raise)
        remove_method :raise
        define_method :raise do |*args|
          begin
            old_raise.call(*args)
          ensure
            if $!
              lib = File.expand_path("..", __FILE__)
              $!.backtrace.reject! { |line| line.start_with?(lib) }
            end
          end
        end
      end
    end

    def print_exception(stream, error)
      first, rest = error.backtrace.first, error.backtrace.drop(1)
      stream.puts("#{first}: #{error} (#{error.class})")
      rest.each { |line| stream.puts("\tfrom #{line}") }
    end

    def with_pty
      PTY.open do |master, slave|
        [STDOUT, STDERR, STDIN].each { |s| s.reopen slave }
        yield
        reset_streams
      end
    end

    def reset_streams
      [STDOUT, STDERR].each { |stream| stream.reopen(spring_env.log_file) }
      STDIN.reopen("/dev/null")
    end

    def wait(pid, streams, client)
      @mutex.synchronize { @waiting << pid }

      # Wait in a separate thread so we can run multiple commands at once
      Thread.new {
        begin
          _, status = Process.wait2 pid
          log "#{pid} exited with #{status.exitstatus}"

          streams.each(&:close)
          client.puts(status.exitstatus)
          client.close
        ensure
          @mutex.synchronize { @waiting.delete pid }
          exit_if_finished
        end
      }
    end

    private

    def active_record_configured?
      defined?(ActiveRecord::Base) && ActiveRecord::Base.configurations.any?
    end
  end
end