上一章我们主要了解了spring的client部分的代码,那么这一张我们从server_boot开始了解spring server部分的代码。
boot_server主要是运行了如下命令:
def server_command
ENV["SPRING_SERVER_COMMAND"] || "#{File.expand_path("../../../bin/spring", __FILE__)} server --background"
end
也就是Client::Server文件
module Spring
module Client
class Server < Command
def self.description
"Explicitly start a Spring server in the foreground"
end
def call
require "spring/server"
Spring::Server.boot(foreground: foreground?)
end
def foreground?
!args.include?("--background")
end
end
end
end
然后看到这句代码Spring::Server.boot(foreground: foreground?):
# spring/server
def self.boot(options = {})
new(options).boot
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
boot设置好pid hook title等之后调用了start_server
def start_server
server = UNIXServer.open(env.socket_name)
log "started on #{env.socket_name}"
loop { serve server.accept }
rescue Interrupt
end
1.打开了UNIXServer就是上一张我们client需要连接的那个socket:
2.等在server.accept到client发过来的信息:
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 = command.values_at('args', 'default_rails_env')
if Spring.command?(args.first)
log "running command #{args.first}"
client.puts
client.puts @applications[rails_env_for(args, default_rails_env)].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
serve里主要的工作:
1.发送version给client
2.收到client那边send_io过来的对象
3.拿到client发送过来的command
4.@applications[rails_env_for(args, default_rails_env)].run(app_client),根据环境执行相应的command.
看到ApplicationManager里的run方法:
最后开启了一个新的线程,主要执行了 application/boot里的代码,并且将child_socket设置成文件描述符3,日志的socket设置成文件描述符4.
def run(client)
with_child do
child.send_io client
child.gets or raise Errno::EPIPE
end
pid = child.gets.to_i
unless pid.zero?
log "got worker pid #{pid}"
pid
end
rescue Errno::ECONNRESET, Errno::EPIPE => e
log "#{e} while reading from child; returning no pid"
nil
ensure
client.close
end
def with_child
synchronize do
if alive?
begin
yield
rescue Errno::ECONNRESET, Errno::EPIPE
# The child has died but has not been collected by the wait thread yet,
# so start a new child and try again.
log "child dead; starting"
start
yield
end
else
log "child not running; starting"
start
yield
end
end
end
def start
start_child
end
def start_child(preload = false)
@child, child_socket = UNIXSocket.pair
Bundler.with_clean_env do
@pid = Process.spawn(
{
"RAILS_ENV" => app_env,
"RACK_ENV" => app_env,
"SPRING_ORIGINAL_ENV" => JSON.dump(Spring::ORIGINAL_ENV),
"SPRING_PRELOAD" => preload ? "1" : "0"
},
"ruby",
"-I", File.expand_path("../..", $LOADED_FEATURES.grep(/bundler\/setup\.rb$/).first),
"-I", File.expand_path("../..", __FILE__),
"-e", "require 'spring/application/boot'",
3 => child_socket,
4 => spring_env.log_file,
)
end
start_wait_thread(pid, child) if child.gets
child_socket.close
end
def start_wait_thread(pid, child)
Process.detach(pid)
Spring.failsafe_thread {
# The recv can raise an ECONNRESET, killing the thread, but that's ok
# as if it does we're no longer interested in the child
loop do
IO.select([child])
break if child.recv(1, Socket::MSG_PEEK).empty?
sleep 0.01
end
log "child #{pid} shutdown"
synchronize {
if @pid == pid
@pid = nil
restart
end
}
}
end
以上代码最后主要是为了开启一个进程,那么我们看下 spring/application/boot里的代码:
# This is necessary for the terminal to work correctly when we reopen stdin.
Process.setsid
require "spring/application"
app = Spring::Application.new(
UNIXSocket.for_fd(3),
Spring::JSON.load(ENV.delete("SPRING_ORIGINAL_ENV").dup),
Spring::Env.new(log_file: IO.for_fd(4))
)
Signal.trap("TERM") { app.terminate }
Spring::ProcessTitleUpdater.run { |distance|
"spring app | #{app.app_name} | started #{distance} ago | #{app.app_env} mode"
}
app.eager_preload if ENV.delete("SPRING_PRELOAD") == "1"
app.run
实例化一个application类,然后调用run方法:
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
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?)
# Rails 5.1 forward-compat. AD::R is deprecated to AS::R in Rails 5.
if defined? ActiveSupport::Reloader
Rails.application.reloader.reload!
else
ActionDispatch::Reloader.cleanup!
ActionDispatch::Reloader.prepare!
end
end
pid = fork {
Process.setsid
IGNORE_SIGNALS.each { |sig| trap(sig, "DEFAULT") }
trap("TERM", "DEFAULT")
STDERR.puts "Running via Spring preloader in process #{Process.pid}" unless Spring.quiet
ARGV.replace(args)
$0 = command.exec_name
# 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
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
def wait(pid, streams, client)
@mutex.synchronize { @waiting << pid }
# Wait in a separate thread so we can run multiple commands at once
Spring.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
}
Spring.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
def reset_streams
[STDOUT, STDERR].each { |stream| stream.reopen(spring_env.log_file) }
STDIN.reopen("/dev/null")
end
command.call调用了rails console的命令,将console加入到ARGV,然后load rails 目录下的bin/rails文件,这时就会执行rails c 命令,显然这有点绕看了好几次才明白。
def call
ARGV.unshift command_name
load Dir.glob(::Rails.root.join("{bin,script}/rails")).first
end
附上流程图: