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