目前关于rails的启动过程中的调研博客,大多停留在rails 5版本,但是当前的rails 7版本的启动过程已经有了更新的改变,因此对这一过程进行了进一步的调研。
ruby版本:ruby 3.0.0
rails版本:rails 7.0.4
本文中的路径和包版本均为本人电脑中的版本
找到rails
首先查看rails命令的路径,执行 which rails 命令,可以得到
1
/home/zhengmingyu/.rvm/gems/ruby-3.0.0/bin/rails
打开rails文件,可以看到其中的内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# The application 'railties' is installed as part of a gem, and
# this file is here to facilitate running it.
#
require 'rubygems'
version = ">= 0.a"
str = ARGV.first
if str
str = str.b[/\A_(.*)_\z/, 1]
if str and Gem::Version.correct?(str)
version = str
ARGV.shift
end
end
if Gem.respond_to?(:activate_bin_path)
load Gem.activate_bin_path('railties', 'rails', version)
else
gem "railties", version
load Gem.bin_path("railties", "rails", version)
end
在其中加载了
/home/zhengmingyu/.rvm/gems/ruby-3.0.0/gems/railties-7.0.4/exe/rails
这一文件的内容
1
2
3
4
5
6
7
8
9
10
#!/usr/bin/env ruby
# frozen_string_literal: true
git_path = File.expand_path("../../.git", __dir__)
if File.exist?(git_path)
railties_path = File.expand_path("../lib", __dir__)
$:.unshift(railties_path)
end
require "rails/cli"
其中最重要的是 require "rails/cli"
,它位于/home/zhengmingyu/.rvm/gems/ruby-3.0.0/gems/railties-7.0.4/lib/rails/cli.rb
查看 cli.rb 中的内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# frozen_string_literal: true
require "rails/app_loader"
# If we are inside a Rails application this method performs an exec and thus
# the rest of this script is not run.
Rails::AppLoader.exec_app
require "rails/ruby_version_check"
Signal.trap("INT") { puts; exit(1) }
require "rails/command"
if ARGV.first == "plugin"
ARGV.shift
Rails::Command.invoke :plugin, ARGV
else
Rails::Command.invoke :application, ARGV
end
其中,Rails::AppLoader.exec_app
执行了app加载任务,在这里,rails开始从我们创建的app里面获取项目信息来准备启动。
server命令
我们可以查看exec_app
函数的内容,它将 APP_PATH
设置为 config/application
,然后加载 config/boot
和 rails/commands
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def exec_app
original_cwd = Dir.pwd
loop do
if exe = find_executable
contents = File.read(exe)
if /(APP|ENGINE)_PATH/.match?(contents)
exec RUBY, exe, *ARGV
break # non reachable, hack to be able to stub exec in the test suite
elsif exe.end_with?("bin/rails") && contents.include?("This file was generated by Bundler")
$stderr.puts(BUNDLER_WARNING)
Object.const_set(:APP_PATH, File.expand_path("config/application", Dir.pwd))
require File.expand_path("../boot", APP_PATH)
require "rails/commands"
break
end
end
# If we exhaust the search there is no executable, this could be a
# call to generate a new application, so restore the original cwd.
Dir.chdir(original_cwd) && return if Pathname.new(Dir.pwd).root?
# Otherwise keep moving upwards in search of an executable.
Dir.chdir("..")
end
end
执行程序需要 require "rails/commands"
,它位于 /home/zhengmingyu/.rvm/gems/ruby-3.0.0/gems/railties-7.0.4/lib/rails/commands.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# frozen_string_literal: true
require "rails/command"
aliases = {
"g" => "generate",
"d" => "destroy",
"c" => "console",
"s" => "server",
"db" => "dbconsole",
"r" => "runner",
"t" => "test"
}
command = ARGV.shift
command = aliases[command] || command
Rails::Command.invoke command, ARGV
我们可以看到在commands.rb中,我们的命令 rails s
等价于 rails server
,并调用了invoke
方法,该方法位于 /home/zhengmingyu/.rvm/gems/ruby-3.0.0/gems/railties-7.0.4/lib/rails/command.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# Receives a namespace, arguments, and the behavior to invoke the command.
def invoke(full_namespace, args = [], **config)
namespace = full_namespace = full_namespace.to_s
if char = namespace =~ /:(\w+)$/
command_name, namespace = $1, namespace.slice(0, char)
else
command_name = namespace
end
command_name, namespace = "help", "help" if command_name.blank? || HELP_MAPPINGS.include?(command_name)
command_name, namespace, args = "application", "application", ["--help"] if rails_new_with_no_path?(args)
command_name, namespace = "version", "version" if %w( -v --version ).include?(command_name)
original_argv = ARGV.dup
ARGV.replace(args)
command = find_by_namespace(namespace, command_name)
if command && command.all_commands[command_name]
command.perform(command_name, args, config)
else
args = ["--describe", full_namespace] if HELP_MAPPINGS.include?(args[0])
find_by_namespace("rake").perform(full_namespace, args, config)
end
ensure
ARGV.replace(original_argv)
end
在该方法中,调用了同文件下的方法 find_by_namespace
,获取了server对应的command内容,对应server_command.rb的内容,该文件位于: /home/zhengmingyu/.rvm/gems/ruby-3.0.0/gems/railties-7.0.4/lib/rails/commands/server/server_command.rb
得到command
之后调用了其中的perform
方法,在server_command.rb
中该方法的内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def perform
extract_environment_option_from_argument
set_application_directory!
prepare_restart
Rails::Server.new(server_options).tap do |server|
# Require application after server sets environment to propagate
# the --environment option.
require APP_PATH
Dir.chdir(Rails.application.root)
if server.serveable?
print_boot_information(server.server, server.served_url)
after_stop_callback = -> { say "Exiting" unless options[:daemon] }
server.start(after_stop_callback)
else
say rack_server_suggestion(options[:using])
end
end
end
回顾app
在rails官方文档中,给出了app的启动过程:
Booting process
The application is also responsible for setting up and executing the booting process. From the moment you require
config/application.rb
in your app, the booting process goes like this:
require "config/boot.rb"
to set up load paths.require
railties and engines.- Define
Rails.application
asclass MyApp::Application < Rails::Application
.- Run
config.before_configuration
callbacks.- Load
config/environments/ENV.rb
.- Run
config.before_initialize
callbacks.- Run
Railtie#initializer
defined by railties, engines, and application. One by one, each engine sets up its load paths and routes, and runs itsconfig/initializers/*
files.- Custom
Railtie#initializers
added by railties, engines, and applications are executed.- Build the middleware stack and run
to_prepare
callbacks.- Run
config.before_eager_load
andeager_load!
ifeager_load
istrue
.- Run
config.after_initialize
callbacks.
在server_command.rb
中perform
方法中我们看到了set_application_directory
将程序目录指定为包含了 config.ru
的目录,然后require APP_PATH
即引入了config/application.rb
,在该文件中则引入了config/boot.rb
,其内容如下:
1
2
3
4
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
require "bundler/setup" # Set up gems listed in the Gemfile.
require "bootsnap/setup" # Speed up boot time by caching expensive operations.
在这里通过 Bundler.setup 将 Gemfile 中的 gem 路径添加到加载路径。
继续在server_command.rb
中perform
方法中查看,调用server.start
方法
1
2
3
4
5
6
7
8
9
10
def start(after_stop_callback = nil)
trap(:INT) { exit }
create_tmp_directories
setup_dev_caching
log_to_stdout if options[:log_stdout]
super()
ensure
after_stop_callback.call if after_stop_callback
end
Rails::Server
是继承自Rack::Server
的,所以start方法最后的super()
是调用Rack::Server
中的start方法。该方法位于rack下的server.rb中,路径为/home/zhengmingyu/.rvm/gems/ruby-3.0.0/gems/rack-2.2.4/lib/rack/server.rb
,我们可以查看该方法的内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def start(&block)
if options[:warn]
$-w = true
end
if includes = options[:include]
$LOAD_PATH.unshift(*includes)
end
Array(options[:require]).each do |library|
require library
end
if options[:debug]
$DEBUG = true
require 'pp'
p options[:server]
pp wrapped_app
pp app
end
check_pid! if options[:pid]
# Touch the wrapped app, so that the config.ru is loaded before
# daemonization (i.e. before chdir, etc).
handle_profiling(options[:heapfile], options[:profile_mode], options[:profile_file]) do
wrapped_app
end
daemonize_app if options[:daemonize]
write_pid if options[:pid]
trap(:INT) do
if server.respond_to?(:shutdown)
server.shutdown
else
exit
end
end
server.run(wrapped_app, **options, &block)
end
其中最重要的是获取wrapped_app
,并在最后调用server.run(wrapped_app, **options, &block)
。
在wrapped_app
中,执行了我们app的config.ru
,config.ru
内容如下:
1
2
3
4
5
6
# This file is used by Rack-based servers to start the application.
require_relative "config/environment"
run Rails.application
Rails.application.load_server
至此,app已经创建完成。
选择服务器
在上述start
方法中的最后将app实例通过server.run
方法进行调用,server
方法通过 Rack::Handler
选择服务器,位于/home/zhengmingyu/.rvm/gems/ruby-3.0.0/gems/rack-2.2.4/lib/rack/handler.rb
,有关内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
def self.get(server)
return unless server
server = server.to_s
unless @handlers.include? server
load_error = try_require('rack/handler', server)
end
if klass = @handlers[server]
const_get(klass)
else
const_get(server, false)
end
rescue NameError => name_error
raise load_error || name_error
end
# Select first available Rack handler given an `Array` of server names.
# Raises `LoadError` if no handler was found.
#
# > pick ['thin', 'webrick']
# => Rack::Handler::WEBrick
def self.pick(server_names)
server_names = Array(server_names)
server_names.each do |server_name|
begin
return get(server_name.to_s)
rescue LoadError, NameError
end
end
raise LoadError, "Couldn't find handler for: #{server_names.join(', ')}."
end
SERVER_NAMES = %w(puma thin falcon webrick).freeze
private_constant :SERVER_NAMES
def self.default
# Guess.
if ENV.include?("PHP_FCGI_CHILDREN")
Rack::Handler::FastCGI
elsif ENV.include?(REQUEST_METHOD)
Rack::Handler::CGI
elsif ENV.include?("RACK_HANDLER")
self.get(ENV["RACK_HANDLER"])
else
pick SERVER_NAMES
end
end
在该default
方法中,如果ENV中没有设置,那么调用pick
方法,按照 puma thin falcon webrick 的顺序选择,应用get方法进行选择,查看@handlers
中是否有匹配的,如没有则尝试通过 try_require('rack/handler', server)
加载。
当我们加载puma成功之后,返回到server.run
方法,它实际上是执行了 Rack::Handler::Puma.run
,我们可以在gems中的puma包下找到,路径为 /home/zhengmingyu/.rvm/gems/ruby-3.0.0/gems/puma-5.6.5/lib/rack/handler/puma.rb
,对应方法的调用过程如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def self.run(app, **options)
conf = self.config(app, options)
events = options.delete(:Silent) ? ::Puma::Events.strings : ::Puma::Events.stdio
launcher = ::Puma::Launcher.new(conf, :events => events)
yield launcher if block_given?
begin
launcher.run
rescue Interrupt
puts "* Gracefully stopping, waiting for requests to finish"
launcher.stop
puts "* Goodbye!"
end
end
至此,服务启动完成。