Skip to content

Instantly share code, notes, and snippets.

@xiangzhuyuan
Last active September 13, 2016 01:43
Show Gist options
  • Save xiangzhuyuan/fd9617633b660c632dfd to your computer and use it in GitHub Desktop.
Save xiangzhuyuan/fd9617633b660c632dfd to your computer and use it in GitHub Desktop.
when you type 'rails server' and click enter

##################

此文 主要目的就是一探究竟, 到底rails server之后发生了什么. 而正文里的摘取了 rails 和rspec 这个2个可执行问价你的内容是什么,

就这就往下看了.

##################

#!/usr/bin/env ruby
#
# This file was generated by RubyGems.
#
# The application 'railties' is installed as part of a gem, and
# this file is here to facilitate running it.
#

require 'rubygems'

version = ">= 0"

if ARGV.first
  str = ARGV.first
  str = str.dup.force_encoding("BINARY") if str.respond_to? :force_encoding
  if str =~ /\A_(.*)_\z/ and Gem::Version.correct?($1) then
    version = $1
    ARGV.shift
  end
end

gem 'railties', version
load Gem.bin_path('railties', 'rails', version)

########

#!/usr/bin/env ruby_executable_hooks
#
# This file was generated by RubyGems.
#
# The application 'rspec-core' is installed as part of a gem, and
# this file is here to facilitate running it.
#

require 'rubygems'

version = ">= 0"

if ARGV.first
  str = ARGV.first
  str = str.dup.force_encoding("BINARY") if str.respond_to? :force_encoding
  if str =~ /\A_(.*)_\z/ and Gem::Version.correct?($1) then
    version = $1
    ARGV.shift
  end
end

gem 'rspec-core', version
load Gem.bin_path('rspec-core', 'rspec', version)

#$HOME/.rvm/gems/ruby-2.1.2/gems/railties-4.1.1/bin

#!/usr/bin/env ruby

git_path = File.expand_path('../../../.git', __FILE__) # 这个方法和奇葩,我总是搞不明白, 从当前文件算起,往上三层.
#-----$HOME/.rvm/gems/ruby-2.1.2/gems/railties-4.1.1/bin/rails 
#`--- $HOME/.rvm/gems/ruby-2.1.2/gems/.git

if File.exist?(git_path)
  railties_path = File.expand_path('../../lib', __FILE__)
#----$HOME/.rvm/gems/ruby-2.1.2/gems/railties-4.1.1/bin/rails
# `--$HOME/.rvm/gems/ruby-2.1.2/gems/railties-4.1.1/lib
  $:.unshift(railties_path)
end
require "rails/cli" #接着就是这里

~/.rvm/gems/ruby-2.1.2/gems/railties-4.1.1/lib/rails/cli.rb


require 'rails/app_rails_loader'

# If we are inside a Rails application this method performs an exec and thus
# the rest of this script is not run.
Rails::AppRailsLoader.exec_app_rails
# 这个 exec_app_rails 方法干什么的呢?
# 我们接着看rails/app_rails_loader这个文件
require 'rails/ruby_version_check'
Signal.trap("INT") { puts; exit(1) }

if ARGV.first == 'plugin'
  ARGV.shift
  require 'rails/commands/plugin'
else
  require 'rails/commands/application'
end

require 'pathname'

module Rails
  module AppRailsLoader
    RUBY = Gem.ruby
    EXECUTABLES = ['bin/rails', 'script/rails']
    BUNDLER_WARNING = <<EOS
Looks like your app's ./bin/rails is a stub that was generated by Bundler.

In Rails 4, your app's bin/ directory contains executables that are versioned
like any other source code, rather than stubs that are generated on demand.

Here's how to upgrade:

  bundle config --delete bin    # Turn off Bundler's stub generator
  rake rails:update:bin         # Use the new Rails 4 executables
  git add bin                   # Add bin/ to source control

You may need to remove bin/ from your .gitignore as well.

When you install a gem whose executable you want to use in your app,
generate it and add it to source control:

  bundle binstubs some-gem-name
  git add bin/new-executable

EOS

    def self.exec_app_rails
      original_cwd = Dir.pwd

      loop do
        if exe = find_executable
          contents = File.read(exe)

          if contents =~ /(APP|ENGINE)_PATH/
            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) and return if Pathname.new(Dir.pwd).root?

        # Otherwise keep moving upwards in search of an executable.
        Dir.chdir('..')
      end
    end

    def self.find_executable
      EXECUTABLES.find { |exe| File.file?(exe) }
    end
  end
end


其实 rails3代 和rails4代差别还挺大的呢!


Looks like your app's ./bin/rails is a stub that was generated by Bundler.

In Rails 4, your app's bin/ directory contains executables that are versioned
like any other source code, rather than stubs that are generated on demand.

Here's how to upgrade:

bundle config --delete bin # Turn off Bundler's stub generator
rake rails:update:bin # Use the new Rails 4 executables
git add bin # Add bin/ to source control

You may need to remove bin/ from your .gitignore as well.

When you install a gem whose executable you want to use in your app,
generate it and add it to source control:

bundle binstubs some-gem-name
git add bin/new-executable

rails 3里有个 script 文件夹,那个里面有 rails 命令. 而在4里会 bin 这个目录 这个地方会有之前 代替 script/rails 的东西.


EXECUTABLES = ['bin/rails', 'script/rails']


def self.find_executable EXECUTABLES.find { |exe| File.file?(exe) } end


看看这个 文件内容:

#!/usr/bin/env ruby
begin
  load File.expand_path("../spring", __FILE__)
rescue LoadError
end
APP_PATH = File.expand_path('../../config/application',  __FILE__)
require_relative '../config/boot'
require 'rails/commands'

这个里面有 APP_PATH, 可以看见.

  loop do
        if exe = find_executable
          contents = File.read(exe)

          if contents =~ /(APP|ENGINE)_PATH/
            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) # 基于 app_path, 获取 boot 文件,并加载
            require 'rails/commands' #下面我们研究这个.
            break
          end
        end

兜了一圈都是 rails 自己的事情,现在终于要回到app based on rails了, 为什么这么说呢,因为你之前的这些都看不见的啊.

上面的代码段中提到的 app_path 了. 什么 config/application 了 什么 config/boot 了, 这些相信抛开之前 rails 自己的代码,都能够大概明白什么意思了,到了这一步,也就是当前项目本身的一些初始化了.


我们先看看 require 的这个 boot 文件,内容是什么的呢?

In a standard Rails application, there's a Gemfile which declares all dependencies of the application. config/boot.rb sets ENV['BUNDLE_GEMFILE'] to the location of this file. If the Gemfile exists, then bundler/setup is required. The require is used by Bundler to configure the load path for your Gemfile's dependencies.

这个说明什么了呢,为什么叫 boot 这个名字呢?为什么,便可知,其实 boot 就是一些之根本了,就是从当前文件位置获取到 Gemfile 的位置,然后查看是否存在,然后利用 bundle 来管理本项目的依赖


接下来我们看看这个文件

rails/commands

ARGV << '--help' if ARGV.empty?
 
aliases = {
  "g"  => "generate",
  "d"  => "destroy",
  "c"  => "console",
  "s"  => "server",
  "db" => "dbconsole",
  "r"  => "runner"
}
 
command = ARGV.shift
command = aliases[command] || command
 
require 'rails/commands/commands_tasks'
 
Rails::CommandsTasks.new(ARGV).run_command!(command)

上面的代码有2目的:

  1. 给一些命令添加别名,好使!
  2. require 'rails/commands/commands_tasks'

继续看看这个文件 rails/commands/command_tasks.rb

这个文件作用有2:

  1. 如果给的命令不在 list 中就负责扔一个异常帮助信息给他.
  2. 命令对了就继续往下执行那个命令.

我们现在在研究 rails server 这个命令,那我们继续往下:

# Change to the application's path if there is no config.ru file in current directory.
# This allows us to run `rails server` from other directories, but still get
# the main config.ru and properly set the tmp directory.
def set_application_directory!
  Dir.chdir(File.expand_path('../../', APP_PATH)) unless File.exist?(File.expand_path("config.ru"))
end
 
def server
  set_application_directory! #这个可以让你在其他命令也可以执行 rails 命令
  require_command!("server")
 
  Rails::Server.new.tap do |server|
    require APP_PATH
    Dir.chdir(Rails.application.root)
    server.start
  end
end
 
def require_command!(command)
  require "rails/commands/#{command}"
end

看看这个 rails/commands/server.rb

require 'fileutils' #
require 'optparse' #此二就是 ruby 里正常库了. 用来处理文件和解析参数之用!!!
require 'action_dispatch' # 这个就是我们接下来研究的.
 
module Rails
  class Server < ::Rack::Server #只简单继承,没有任何实现.

直接这样require 'action_dispatch' 会在哪里呢? 首先这个actionpack 它是作为单独的 gem 存在的.是 rails 框架的一个组件,他的作用呢,就是为了负责路由,会话, 还有还有一些通用中间件的工作.

这里有个以为,require 之后发生了什么?没有讲!

接下来还是回到 server.rb 这个文件里.


大概看了一眼 action_dispatch, 这个加载了老多东西啊. action_support 啊 , rack 啊!

还是回到主要的. Rails::Server 急继承自 Rack::Server 的. 在 Rails::Server 被 new 的时候,调用初始化方法:

def initialize(*)
  super
  set_environment #重点!!!
end
  1. 首先调用的就是 Rack::Server 的初始化方法了:
def initialize(options = nil)
  @options = options
  @app = options[:app] if options && options[:app]
end

这个地方 options就是空的,什么都没有发生,还是回到 Rails::Server 那里去.

看看这个看上去简单的方法做了些什么呢?乍一看,其实什么都没有的.
def set_environment
  ENV["RAILS_ENV"] ||= options[:environment] #这个地方坑爹啊,看上去 `options` 就是个数组是不是? 还复数的, 殊不知™是方法! shit!
end

而事实上定义在Rack::Server里的这个 options 方法牛了逼了,干了很多活的!我们来瞧瞧:

def options
  @options ||= parse_options(ARGV) #看下面
end

#parse_options
def parse_options(args)
  options = default_options #看下面
 
  # Don't evaluate CGI ISINDEX parameters.
  # http://www.meb.uni-bonn.de/docs/cgi/cl.html
  args.clear if ENV.include?("REQUEST_METHOD")
 
  options.merge! opt_parser.parse! args #看下面
  options[:config] = ::File.expand_path(options[:config])
  ENV["RACK_ENV"] = options[:environment]
  options
end

# default_options
def default_options
  {
    environment: ENV['RACK_ENV'] || "development",
    pid:         nil,
    Port:        9292,
    Host:        "0.0.0.0",
    AccessLog:   [],
    config:      "config.ru"
  }
end

def opt_parser
  Options.new #这个类定义在 Rack::Server 里
end

#但是它的方法 `parse!` 在 Rails::Server 里被重写了.
def parse!(args)
  args, options = args.dup, {}
 
  opt_parser = OptionParser.new do |opts|
    opts.banner = "Usage: rails server [mongrel, thin, etc] [options]"
    opts.on("-p", "--port=port", Integer,
            "Runs Rails on the specified port.", "Default: 3000") { |v| options[:Port] = v }
  ...

至此,要回到最初提到 APP_PATHconfig/application了.


这里,这个 config/application.rb 就要被执行了.

看到这么一句话

1.11 Rails::Server#start
After config/application is loaded, server.start is called. This method is defined like this:

哪里来的 server.start 呢?我就莫名其妙不是.原来尼玛就在上面呢.

 Rails::Server.new.tap do |server|
    require APP_PATH
    Dir.chdir(Rails.application.root)
    server.start
  end

看见了吧, server.start 啊!

他就是在 之前提到的 rails/commands/server.rb

def start
  print_boot_information
  trap(:INT) { exit }
  create_tmp_directories
  log_to_stdout if options[:log_stdout]
 
  super
  ...
end
 
    private

      def print_boot_information
        url = "#{options[:SSLEnable] ? 'https' : 'http'}://#{options[:Host]}:#{options[:Port]}"
        puts "=> Booting #{ActiveSupport::Inflector.demodulize(server)}"
        puts "=> Rails #{Rails.version} application starting in #{Rails.env} on #{url}"
        puts "=> Run `rails server -h` for more startup options"

        if options[:Host].to_s.match(/0\.0\.0\.0/)
          puts "=> Notice: server is listening on all interfaces (#{options[:Host]}). Consider using 127.0.0.1 (--binding option)"
        end

        puts "=> Ctrl-C to shutdown server" unless options[:daemonize]
      end

      def create_tmp_directories
        %w(cache pids sessions sockets).each do |dir_to_make|
          FileUtils.mkdir_p(File.join(Rails.root, 'tmp', dir_to_make))
        end
      end

      def log_to_stdout
        wrapped_app # touch the app so the logger is set up

        console = ActiveSupport::Logger.new($stdout)
        console.formatter = Rails.logger.formatter
        console.level = Rails.logger.level

        Rails.logger.extend(ActiveSupport::Logger.broadcast(console))
      end

上面代码干了几件事情:

  1. 输出最初的启动信息
  2. 创建一个INT 信号,所以 ctrl+c 就停止进程了.
  3. 创建 tmp 下的临时目录
  4. 初始化 log

完了之后,调用能够了 super 的 start 方法, 在 Rack:Server 里:

def start &blk
  if options[:warn]
    $-w = true
  end
 
  if includes = options[:include]
    $LOAD_PATH.unshift(*includes)
  end
 
  if library = options[:require]
    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).
  wrapped_app
 
  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, &blk
end

有意思的部分就是最后一行了:server.run wrapped_app, options, &blk 它又调用了一次 wrapped_app.看看究竟

    def app
      @app ||= begin
        app = super
        app.respond_to?(:to_app) ? app.to_app : app
      end
    end

多次用到这个 options[:config],它默认的就是指向 config.ru 的.

# This file is used by Rack-based servers to start the application.

require ::File.expand_path('../config/environment',  __FILE__)
run Rails.application

上面有提到了 config/environment 文件,我们再次回到熟悉的地方: 这个地方算是一个原点,不管是那种 app server 启动,都是跑完 rails 之后再从这个出发. 首先就是 require config/application

而这个里面又从 config/boot 开始.当然我们都知道刚才从 rails server 那种方式,他已经跑过了,而以 passenger 启动的就还需要跑.

#config/application.rb 再往下就是 require 'rails/all'了 这个文件railties/lib/rails/all.rb可以看出就是加载组件了:

require "rails"
 
%w(
    active_record
    action_controller
    action_mailer
    rails/test_unit
    sprockets
).each do |framework|
  begin
    require "#{framework}/railtie"
  rescue LoadError
  end
end

再往下执行就是 Rails.application 的 configuration 了. 走完上面的

#再回到 config/environment

你会发现有这么一个 APP_NAME::Application.initialize!

这个 initialize!方法在哪里? ###railties/lib/rails/application.rb

def initialize!(group=:default) #:nodoc:
  raise "Application has been already initialized." if @initialized
  run_initializers(group, self)
  @initialized = true
  self
end

####railties/lib/rails/initializable.rb

def run_initializers(group=:default, *args)
  return if instance_variable_defined?(:@ran)
  initializers.tsort_each do |initializer|
    initializer.run(*args) if initializer.belongs_to?(group)
  end
  @ran = true
end

到这里,我们还要回到 Rack::Server 那里去

至此, 这个 app 对象就是 Rails app 了.接下来就是 Rack 和中间件的事情了.

      def build_app(app)
        middleware[options[:environment]].reverse_each do |middleware|
          middleware = middleware.call(self) if middleware.respond_to?(:call)
          next unless middleware
          klass, *args = middleware
          app = klass.new(app, *args)
        end
        app
      end

#记住上面的 build_app 它是有 wrapped_app调用的.而 server.run 这个实现就取决于你用哪个 appserver 了.如果你用Mongrel,那实现就如下:

def self.run(app, options={})
  server = ::Mongrel::HttpServer.new(
    options[:Host]           || '0.0.0.0',
    options[:Port]           || 8080,
    options[:num_processors] || 950,
    options[:throttle]       || 0,
    options[:timeout]        || 60)
  # Acts like Rack::URLMap, utilizing Mongrel's own path finding methods.
  # Use is similar to #run, replacing the app argument with a hash of
  # { path=>app, ... } or an instance of Rack::URLMap.
  if options[:map]
    if app.is_a? Hash
      app.each do |path, appl|
        path = '/'+path unless path[0] == ?/
        server.register(path, Rack::Handler::Mongrel.new(appl))
      end
    elsif app.is_a? URLMap
      app.instance_variable_get(:@mapping).each do |(host, path, appl)|
       next if !host.nil? && !options[:Host].nil? && options[:Host] != host
       path = '/'+path unless path[0] == ?/
       server.register(path, Rack::Handler::Mongrel.new(appl))
      end
    else
      raise ArgumentError, "first argument should be a Hash or URLMap"
    end
  else
    server.register('/', Rack::Handler::Mongrel.new(app))
  end
  yield server if block_given?
  server.run.join
end

#THE END

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment