File: //usr/share/rubygems-integration/all/gems/spring-2.1.1/lib/spring/client/run.rb
require "rbconfig"
require "socket"
require "bundler"
module Spring
  module Client
    class Run < Command
      FORWARDED_SIGNALS = %w(INT QUIT USR1 USR2 INFO WINCH) & Signal.list.keys
      CONNECT_TIMEOUT   = 1
      BOOT_TIMEOUT      = 20
      attr_reader :server
      def initialize(args)
        super
        @signal_queue  = []
        @server_booted = false
      end
      def log(message)
        env.log "[client] #{message}"
      end
      def connect
        @server = UNIXSocket.open(env.socket_name)
      end
      def call
        begin
          connect
        rescue Errno::ENOENT, Errno::ECONNRESET, Errno::ECONNREFUSED
          cold_run
        else
          warm_run
        end
      ensure
        server.close if server
      end
      def warm_run
        run
      rescue CommandNotFound
        require "spring/commands"
        if Spring.command?(args.first)
          # Command installed since Spring started
          stop_server
          cold_run
        else
          raise
        end
      end
      def cold_run
        boot_server
        connect
        run
      end
      def run
        verify_server_version
        application, client = UNIXSocket.pair
        queue_signals
        connect_to_application(client)
        run_command(client, application)
      rescue Errno::ECONNRESET
        exit 1
      end
      def boot_server
        env.socket_path.unlink if env.socket_path.exist?
        pid     = Process.spawn(gem_env, env.server_command, out: File::NULL)
        timeout = Time.now + BOOT_TIMEOUT
        @server_booted = true
        until env.socket_path.exist?
          _, status = Process.waitpid2(pid, Process::WNOHANG)
          if status
            exit status.exitstatus
          elsif Time.now > timeout
            $stderr.puts "Starting Spring server with `#{env.server_command}` " \
                         "timed out after #{BOOT_TIMEOUT} seconds"
            exit 1
          end
          sleep 0.1
        end
      end
      def server_booted?
        @server_booted
      end
      def gem_env
        bundle = Bundler.bundle_path.to_s
        paths  = Gem.path + ENV["GEM_PATH"].to_s.split(File::PATH_SEPARATOR)
        {
          "GEM_PATH" => [bundle, *paths].uniq.join(File::PATH_SEPARATOR),
          "GEM_HOME" => bundle
        }
      end
      def stop_server
        server.close
        @server = nil
        env.stop
      end
      def verify_server_version
        server_version = server.gets.chomp
        if server_version != env.version
          $stderr.puts "There is a version mismatch between the Spring client " \
                         "(#{env.version}) and the server (#{server_version})."
          if server_booted?
            $stderr.puts "We already tried to reboot the server, but the mismatch is still present."
            exit 1
          else
            $stderr.puts "Restarting to resolve."
            stop_server
            cold_run
          end
        end
      end
      def connect_to_application(client)
        server.send_io client
        send_json server, "args" => args, "default_rails_env" => default_rails_env
        if IO.select([server], [], [], CONNECT_TIMEOUT)
          server.gets or raise CommandNotFound
        else
          raise "Error connecting to Spring server"
        end
      end
      def run_command(client, application)
        log "sending command"
        application.send_io STDOUT
        application.send_io STDERR
        application.send_io STDIN
        send_json application, "args" => args, "env" => ENV.to_hash
        pid = server.gets
        pid = pid.chomp if pid
        # We must not close the client socket until we are sure that the application has
        # received the FD. Otherwise the FD can end up getting closed while it's in the server
        # socket buffer on OS X. This doesn't happen on Linux.
        client.close
        if pid && !pid.empty?
          log "got pid: #{pid}"
          suspend_resume_on_tstp_cont(pid)
          forward_signals(application)
          status = application.read.to_i
          log "got exit status #{status}"
          exit status
        else
          log "got no pid"
          exit 1
        end
      ensure
        application.close
      end
      def queue_signals
        FORWARDED_SIGNALS.each do |sig|
          trap(sig) { @signal_queue << sig }
        end
      end
      def suspend_resume_on_tstp_cont(pid)
        trap("TSTP") {
          log "suspended"
          Process.kill("STOP", pid.to_i)
          Process.kill("STOP", Process.pid)
        }
        trap("CONT") {
          log "resumed"
          Process.kill("CONT", pid.to_i)
        }
      end
      def forward_signals(application)
        @signal_queue.each { |sig| kill sig, application }
        FORWARDED_SIGNALS.each do |sig|
          trap(sig) { forward_signal sig, application }
        end
      end
      def forward_signal(sig, application)
        if kill(sig, application) != 0
          # If the application process is gone, then don't block the
          # signal on this process.
          trap(sig, 'DEFAULT')
          Process.kill(sig, Process.pid)
        end
      end
      def kill(sig, application)
        application.puts(sig)
        application.gets.to_i
      end
      def send_json(socket, data)
        data = JSON.dump(data)
        socket.puts  data.bytesize
        socket.write data
      end
      def default_rails_env
        ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
      end
    end
  end
end