class Spring::Application

Attributes

manager[R]
original_env[R]
spring_env[R]
watcher[R]

Public Class Methods

new(manager, original_env) click to toggle source
# File lib/spring/application.rb, line 9
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

Public Instance Methods

app_env() click to toggle source
# File lib/spring/application.rb, line 31
def app_env
  ENV['RAILS_ENV']
end
app_name() click to toggle source
# File lib/spring/application.rb, line 35
def app_name
  spring_env.app_name
end
connect_database() click to toggle source
# File lib/spring/application.rb, line 251
def connect_database
  ActiveRecord::Base.establish_connection if active_record_configured?
end
disconnect_database() click to toggle source
# File lib/spring/application.rb, line 247
def disconnect_database
  ActiveRecord::Base.remove_connection if active_record_configured?
end
eager_preload() click to toggle source
# File lib/spring/application.rb, line 114
def eager_preload
  with_pty { preload }
end
exit() click to toggle source
# File lib/spring/application.rb, line 214
def exit
  state :exiting
  manager.shutdown(:RDWR)
  exit_if_finished
  sleep
end
exit_if_finished() click to toggle source
# File lib/spring/application.rb, line 221
def exit_if_finished
  @mutex.synchronize {
    Kernel.exit if exiting? && @waiting.empty?
  }
end
exiting?() click to toggle source
# File lib/spring/application.rb, line 51
def exiting?
  @state == :exiting
end
initialized?() click to toggle source
# File lib/spring/application.rb, line 63
def initialized?
  @state == :initialized
end
invoke_after_fork_callbacks() click to toggle source
# File lib/spring/application.rb, line 236
def invoke_after_fork_callbacks
  Spring.after_fork_callbacks.each do |callback|
    callback.call
  end
end
loaded_application_features() click to toggle source
# File lib/spring/application.rb, line 242
def loaded_application_features
  root = Spring.application_root_path.to_s
  $LOADED_FEATURES.select { |f| f.start_with?(root) }
end
log(message) click to toggle source
# File lib/spring/application.rb, line 39
def log(message)
  spring_env.log "[application:#{app_env}] #{message}"
end
preload() click to toggle source
# File lib/spring/application.rb, line 73
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
preload_failed?() click to toggle source
# File lib/spring/application.rb, line 47
def preload_failed?
  @preloaded == :failure
end
preloaded?() click to toggle source
# File lib/spring/application.rb, line 43
def preloaded?
  @preloaded
end
print_exception(stream, error) click to toggle source
reset_streams() click to toggle source
# File lib/spring/application.rb, line 287
def reset_streams
  [STDOUT, STDERR].each { |stream| stream.reopen(spring_env.log_file) }
  STDIN.reopen("/dev/null")
end
run() click to toggle source
# File lib/spring/application.rb, line 118
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
serve(client) click to toggle source
# File lib/spring/application.rb, line 133
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
setup(command) click to toggle source

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.

# File lib/spring/application.rb, line 230
def setup(command)
  if command.setup
    watcher.add loaded_application_features # loaded features may have changed
  end
end
shush_backtraces() click to toggle source

This feels very naughty

# File lib/spring/application.rb, line 256
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
start_watcher() click to toggle source
# File lib/spring/application.rb, line 67
def start_watcher
  @watcher = Spring.watcher
  @watcher.on_stale { state! :watcher_stale }
  @watcher.start
end
state(val) click to toggle source
# File lib/spring/application.rb, line 20
def state(val)
  return if exiting?
  log "#{@state} -> #{val}"
  @state = val
end
state!(val) click to toggle source
# File lib/spring/application.rb, line 26
def state!(val)
  state val
  @interrupt.last.write "."
end
terminate() click to toggle source
# File lib/spring/application.rb, line 203
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
terminating?() click to toggle source
# File lib/spring/application.rb, line 55
def terminating?
  @state == :terminating
end
wait(pid, streams, client) click to toggle source
# File lib/spring/application.rb, line 292
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
watcher_stale?() click to toggle source
# File lib/spring/application.rb, line 59
def watcher_stale?
  @state == :watcher_stale
end
with_pty() { || ... } click to toggle source
# File lib/spring/application.rb, line 279
def with_pty
  PTY.open do |master, slave|
    [STDOUT, STDERR, STDIN].each { |s| s.reopen slave }
    yield
    reset_streams
  end
end

Private Instance Methods

active_record_configured?() click to toggle source
# File lib/spring/application.rb, line 313
def active_record_configured?
  defined?(ActiveRecord::Base) && ActiveRecord::Base.configurations.any?
end