module Spring ORIGINAL_ENV = ENV.to_hash end require "spring/boot" require "spring/application_manager" # Must be last, as it requires bundler/setup, which alters the load path require "spring/commands" module Spring class Server def self.boot(options = {}) new(options).boot end attr_reader :env def initialize(options = {}) @foreground = options.fetch(:foreground, false) @env = options[:env] || default_env @applications = Hash.new do |hash, key| hash[key] = ApplicationManager.new(*key, env) end @pidfile = env.pidfile_path.open('a') @mutex = Mutex.new end def foreground? @foreground end def log(message) env.log "[server] #{message}" end def boot Spring.verify_environment write_pidfile set_pgid unless foreground? ignore_signals unless foreground? set_exit_hook set_process_title start_server end def start_server server = UNIXServer.open(env.socket_name) log "started on #{env.socket_name}" loop { serve server.accept } rescue Interrupt end def serve(client) log "accepted client" client.puts env.version app_client = client.recv_io command = JSON.load(client.read(client.gets.to_i)) args, default_rails_env, spawn_env, reset_env = command.values_at('args', 'default_rails_env', 'spawn_env', 'reset_env') if Spring.command?(args.first) application = @applications[rails_env_for(args, default_rails_env, spawn_env)] reset_if_env_changed(application, reset_env) log "running command #{args.first}" client.puts client.puts application.run(app_client) else log "command not found #{args.first}" client.close end rescue SocketError => e raise e unless client.eof? ensure redirect_output end def rails_env_for(args, default_rails_env, spawn_env) [Spring.command(args.first).env(args.drop(1)) || default_rails_env, spawn_env] end # Boot the server into the process group of the current session. # This will cause it to be automatically killed once the session # ends (i.e. when the user closes their terminal). def set_pgid pgid = Process.getpgid(Process.getsid) Process.setpgid(0, pgid) end # Ignore SIGINT and SIGQUIT otherwise the user typing ^C or ^\ on the command line # will kill the server/application. def ignore_signals IGNORE_SIGNALS.each { |sig| trap(sig, "IGNORE") } end def set_exit_hook server_pid = Process.pid # We don't want this hook to run in any forks of the current process at_exit { shutdown if Process.pid == server_pid } end def shutdown log "shutting down" [env.socket_path, env.pidfile_path].each do |path| if path.exist? path.unlink rescue nil end end @applications.values.map { |a| Spring.failsafe_thread { a.stop } }.map(&:join) end def write_pidfile if @pidfile.flock(File::LOCK_EX | File::LOCK_NB) @pidfile.truncate(0) @pidfile.write("#{Process.pid}\n") @pidfile.fsync else exit 1 end end # We need to redirect STDOUT and STDERR, otherwise the server will # keep the original FDs open which would break piping. (e.g. # `spring rake -T | grep db` would hang forever because the server # would keep the stdout FD open.) def redirect_output [STDOUT, STDERR].each { |stream| stream.reopen(env.log_file) } end def set_process_title ProcessTitleUpdater.run { |distance| "spring server | #{env.app_name} | started #{distance} ago" } end private def reset_if_env_changed(application, reset_env) application.stop if ENV.slice(*reset_env.keys) != reset_env end def default_env Env.new(log_file: default_log_file) end def default_log_file if foreground? && !ENV["SPRING_LOG"] $stdout else nil end end end end