class SpringStandalone::Application

Attributes

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

Public Class Methods

new(manager, original_env, spring_env = Env.new) click to toggle source
# File lib/spring_standalone/application.rb, line 9
def initialize(manager, original_env, spring_env = Env.new)
  @manager      = manager
  @original_env = original_env
  @spring_env   = spring_env
  @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_standalone/application.rb, line 31
def app_env
  ENV['APP_ENV']
end
app_name() click to toggle source
# File lib/spring_standalone/application.rb, line 35
def app_name
  spring_env.app_name
end
connect_database() click to toggle source
# File lib/spring_standalone/application.rb, line 284
def connect_database
  #ActiveRecord::Base.establish_connection if active_record_configured?
end
disconnect_database() click to toggle source
# File lib/spring_standalone/application.rb, line 280
def disconnect_database
  #ActiveRecord::Base.remove_connection if active_record_configured?
end
eager_preload() click to toggle source
# File lib/spring_standalone/application.rb, line 131
def eager_preload
  with_pty { preload }
end
exit() click to toggle source
# File lib/spring_standalone/application.rb, line 247
def exit
  state :exiting
  manager.shutdown(:RDWR)
  exit_if_finished
  sleep
end
exit_if_finished() click to toggle source
# File lib/spring_standalone/application.rb, line 254
def exit_if_finished
  @mutex.synchronize {
    Kernel.exit if exiting? && @waiting.empty?
  }
end
exiting?() click to toggle source
# File lib/spring_standalone/application.rb, line 51
def exiting?
  @state == :exiting
end
initialized?() click to toggle source
# File lib/spring_standalone/application.rb, line 63
def initialized?
  @state == :initialized
end
invoke_after_fork_callbacks() click to toggle source
# File lib/spring_standalone/application.rb, line 269
def invoke_after_fork_callbacks
  SpringStandalone.after_fork_callbacks.each do |callback|
    callback.call
  end
end
loaded_application_features() click to toggle source
# File lib/spring_standalone/application.rb, line 275
def loaded_application_features
  root = SpringStandalone.application_root_path.to_s
  $LOADED_FEATURES.select { |f| f.start_with?(root) }
end
log(message) click to toggle source
# File lib/spring_standalone/application.rb, line 39
def log(message)
  spring_env.log "[application:#{app_env}] #{message}"
end
preload() click to toggle source
# File lib/spring_standalone/application.rb, line 83
def preload
  log "preloading app"

  begin
    require "spring_standalone/commands"
  ensure
    start_watcher
  end

  #require SpringStandalone.application_root_path.join("config", "application")

  # unless Rails.respond_to?(:gem_version) && Rails.gem_version >= Gem::Version.new('5.2.0')
  #   raise "SpringStandalone only supports Rails >= 5.2.0"
  # end

  # # 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 SpringStandalone.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[/^(.*)\:\d+/, 1] }
  raise e unless initialized?
ensure
  watcher.add loaded_application_features
  watcher.add SpringStandalone.gemfile, "#{SpringStandalone.gemfile}.lock"

  # if defined?(Rails) && Rails.application
  #   watcher.add Rails.application.paths["config/initializers"]
  #   watcher.add Rails.application.paths["config/database"]
  #   if secrets_path = Rails.application.paths["config/secrets"]
  #     watcher.add secrets_path
  #   end
  # end
end
preload_failed?() click to toggle source
# File lib/spring_standalone/application.rb, line 47
def preload_failed?
  @preloaded == :failure
end
preloaded?() click to toggle source
# File lib/spring_standalone/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_standalone/application.rb, line 326
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_standalone/application.rb, line 135
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_standalone/application.rb, line 150
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   = SpringStandalone.command(args.shift)

  connect_database
  setup command

  # if Rails.application.reloaders.any?(&:updated?)
  #   Rails.application.reloader.reload!
  # end

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

    unless SpringStandalone.quiet
      STDERR.puts "Running via SpringStandalone preloader in process #{Process.pid}"

      # if Rails.env.production?
      #   STDERR.puts "WARNING: SpringStandalone is running in production. To fix "         \
      #               "this make sure the spring gem is only present "            \
      #               "in `development` and `test` groups in your Gemfile "       \
      #               "and make sure you always use "                             \
      #               "`bundle install --without development test` in production"
      # end
    end

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

    # Delete all env vars which are unchanged from before SpringStandalone 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 SpringStandalone 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

  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
ensure
  # Redirect STDOUT and STDERR to prevent from keeping the original FDs
  # (i.e. to prevent `spring rake -T | grep db` from hanging forever),
  # even when exception is raised before forking (i.e. preloading).
  reset_streams
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_standalone/application.rb, line 263
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_standalone/application.rb, line 289
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
    private :raise
  end
end
start_watcher() click to toggle source
# File lib/spring_standalone/application.rb, line 67
def start_watcher
  @watcher = SpringStandalone.watcher

  @watcher.on_stale do
    state! :watcher_stale
  end

  if @watcher.respond_to? :on_debug
    @watcher.on_debug do |message|
      spring_env.log "[watcher:#{app_env}] #{message}"
    end
  end

  @watcher.start
end
state(val) click to toggle source
# File lib/spring_standalone/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_standalone/application.rb, line 26
def state!(val)
  state val
  @interrupt.last.write "."
end
terminate() click to toggle source
# File lib/spring_standalone/application.rb, line 236
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_standalone/application.rb, line 55
def terminating?
  @state == :terminating
end
wait(pid, streams, client) click to toggle source
# File lib/spring_standalone/application.rb, line 331
def wait(pid, streams, client)
  @mutex.synchronize { @waiting << pid }

  # Wait in a separate thread so we can run multiple commands at once
  SpringStandalone.failsafe_thread {
    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
  }

  SpringStandalone.failsafe_thread {
    while signal = client.gets.chomp
      begin
        Process.kill(signal, -Process.getpgid(pid))
        client.puts(0)
      rescue Errno::ESRCH
        client.puts(1)
      end
    end
  }
end
watcher_stale?() click to toggle source
# File lib/spring_standalone/application.rb, line 59
def watcher_stale?
  @state == :watcher_stale
end
with_pty() { || ... } click to toggle source
# File lib/spring_standalone/application.rb, line 313
def with_pty
  PTY.open do |master, slave|
    [STDOUT, STDERR, STDIN].each { |s| s.reopen slave }
    reader_thread = SpringStandalone.failsafe_thread { master.read }
    begin
      yield
    ensure
      reader_thread.kill
      reset_streams
    end
  end
end