Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rails Boot Process #7

Closed
dylanninin opened this issue Aug 3, 2016 · 1 comment
Closed

Rails Boot Process #7

dylanninin opened this issue Aug 3, 2016 · 1 comment
Assignees
Milestone

Comments

@dylanninin
Copy link
Owner

dylanninin commented Aug 3, 2016

The Rails Command

  • which rails
rails: aliased to _rails_command
  • which _rails_command
_rails_command () {
    if [ -e "bin/rails" ]
    then
        bin/rails $@
    elif [ -e "script/rails" ]
    then
        ruby script/rails $@
    elif [ -e "script/server" ]
    then
        ruby script/$@
    else
        command rails $@
    fi
}
  • bin/rails: base on your rails app's root directory
#!/usr/bin/env ruby
begin
  load File.expand_path('../spring', __FILE__)
rescue LoadError => e
  raise unless e.message.include?('spring')
end
APP_PATH = File.expand_path('../config/application', __dir__)
require_relative '../config/boot'
require 'rails/commands'
  • summary in bin/rails

    • Spring: is a Rails application preloader. It speeds up development by keeping your application running in the background so you don't need to boot it every time you run a test, rake task or migration.
    • APP_PATH: set the absolute path of Rails Application.
    • require_relative '../config/boot': execute bunder setup, install required gems
    ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
    
    require 'bundler/setup' # Set up gems listed in the Gemfile.
    • require 'rails/commands': load rails commands
    ##railties/lib/rails/commands.rb
    ARGV << '--help' if ARGV.empty?
    
    aliases = {
    "g"  => "generate",
    "d"  => "destroy",
    "c"  => "console",
    "s"  => "server",
    "db" => "dbconsole",
    "r"  => "runner",
    "t"  => "test"
    }
    
    command = ARGV.shift
    command = aliases[command] || command
    
    require 'rails/commands/commands_tasks'
    
    Rails::CommandsTasks.new(ARGV).run_command!(command)

Commands Tasks

    #railties/lib/rails/commands/commands_tasks.rb#L45

    def run_command!(command)
      command = parse_command(command)

      if COMMAND_WHITELIST.include?(command)
        send(command)
      else
        run_rake_task(command)
      end
    end
    # railties/lib/rails/commands/commands_tasks.rb#L81
    def server
      set_application_directory!
      require_command!("server")

      Rails::Server.new.tap do |server|
        # We need to require application after the server sets environment,
        # otherwise the --environment option given to the server won't propagate.
        require APP_PATH
        Dir.chdir(Rails.application.root)
        server.start
      end
    end
  • summary in rails server

    • set_application_directory!: set application directory, locate config.ru rack file.
      #railties/lib/rails/commands/commands_tasks.rb#L148
    
      # 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
    • require_command!("server"): require rails server, inherited from Rack Server.
      # railties/lib/rails/commands/commands_tasks.rb#L137
    
      def require_command!(command)
        require "rails/commands/#{command}"
      end
    • server.start: start rack server, than the app in running
    #railties/lib/rails/commands/server.rb#L7
    module Rails
     class Server < ::Rack::Server
      ...
      #railties/lib/rails/commands/server.rb#L51
      def initialize(*)
        super
        set_environment
      end
       ...
      #railties/lib/rails/commands/server.rb#L72
      def start
         print_boot_information
         trap(:INT) { exit }
         create_tmp_directories
         setup_dev_caching
         log_to_stdout if options[:log_stdout]
    
         super
       ensure
         # The '-h' option calls exit before @options is set.
         # If we call 'options' with it unset, we get double help banners.
         puts 'Exiting' unless @options && options[:daemonize]
       end

Rack Server, #4

    # lib/rack/server.rb#L51
    def initialize(options = nil)
      @ignore_options = []

      if options
        @use_default_options = false
        @options = options
        @app = options[:app] if options[:app]
      else
        argv = defined?(SPEC_ARGV) ? SPEC_ARGV : ARGV
        @use_default_options = true
        @options = parse_options(argv)
      end
    end

    def options
      merged_options = @use_default_options ? default_options.merge(@options) : @options
      merged_options.reject { |k, v| @ignore_options.include?(k) }
    end

    def default_options
      environment  = ENV['RACK_ENV'] || 'development'
      default_host = environment == 'development' ? 'localhost' : '0.0.0.0'

      {
        :environment => environment,
        :pid         => nil,
        :Port        => 9292,
        :Host        => default_host,
        :AccessLog   => [],
        :config      => "config.ru"
      }
    end

    def app
      @app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config
    end
    ...
    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

    def server
      @_server ||= Rack::Handler.get(options[:server])

      unless @_server
        @_server = Rack::Handler.default

        # We already speak FastCGI
        @ignore_options = [:File, :Port] if @_server.to_s == 'Rack::Handler::FastCGI'
      end

      @_server
    end
    ...
     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

      def wrapped_app
        @wrapped_app ||= build_app app
      end
    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 ['puma', 'thin', 'webrick']
      end
    end
      ...
      def self.run(app, options={})
        if options[:File]
          STDIN.reopen(UNIXServer.new(options[:File]))
        elsif options[:Port]
          STDIN.reopen(TCPServer.new(options[:Host], options[:Port]))
        end
        FCGI.each { |request|
          serve request, app
        }
      end
      ...
      def self.serve(request, app)
        env = request.env
        env.delete "HTTP_CONTENT_LENGTH"

        env[SCRIPT_NAME] = ""  if env[SCRIPT_NAME] == "/"

        rack_input = RewindableInput.new(request.in)

        env.update(
          RACK_VERSION      => Rack::VERSION,
          RACK_INPUT        => rack_input,
          RACK_ERRORS       => request.err,
          RACK_MULTITHREAD  => false,
          RACK_MULTIPROCESS => true,
          RACK_RUNONCE      => false,
          RACK_URL_SCHEME   => ["yes", "on", "1"].include?(env[HTTPS]) ? "https" : "http"
        )

        env[QUERY_STRING] ||= ""
        env[HTTP_VERSION] ||= env[SERVER_PROTOCOL]
        env[REQUEST_PATH] ||= "/"
        env.delete "CONTENT_TYPE"  if env["CONTENT_TYPE"] == ""
        env.delete "CONTENT_LENGTH"  if env["CONTENT_LENGTH"] == ""

        begin
          status, headers, body = app.call(env)
          begin
            send_headers request.out, status, headers
            send_body request.out, body
          ensure
            body.close  if body.respond_to? :close
          end
        ensure
          rack_input.close
          request.finish
        end
      end
  • config.ru: the rack config file
# This file is used by Rack-based servers to start the application.

require_relative 'config/environment'

run Rails.application
  • config/environment.rb: load and initialize Rails application

    # Load the Rails application.
    require_relative 'application'
    
    # Initialize the Rails application.
    Rails.application.initialize!
  • config/application.rb: the Rails application

    require_relative 'boot'
    
    require "rails"
    # Pick the frameworks you want:
    require "active_model/railtie"
    require "active_job/railtie"
    require "active_record/railtie"
    require "action_controller/railtie"
    require "action_mailer/railtie"
    require "action_view/railtie"
    require "action_cable/engine"
    # require "sprockets/railtie"
    require "rails/test_unit/railtie"
    
    # Require the gems listed in Gemfile, including any gems
    # you've limited to :test, :development, or :production.
    Bundler.require(*Rails.groups)
    
    module MyBackend
    class Application < Rails::Application
      # Settings in config/environments/* take precedence over those specified here.
      # Application configuration should go into files in config/initializers
      # -- all .rb files in that directory are automatically loaded.
    
      # Only loads a smaller set of middleware suitable for API only apps.
      # Middleware like session, flash, cookies can be added back manually.
      # Skip views, helpers and assets when generating a new resource.
      config.api_only = true
      #config.time_zone = 'Beijing'
    
      # i18n config
      config.i18n.load_path += Dir[Rails.root.join('locales', '*.{rb,yml}').to_s]
      config.i18n.default_locale = 'zh-CN'
    end
    end

Rails Application

    # railties/lib/rails/application.rb#L78
    class Application < Engine
    autoload :Bootstrap,              'rails/application/bootstrap'
    autoload :Configuration,          'rails/application/configuration'
    autoload :DefaultMiddlewareStack, 'rails/application/default_middleware_stack'
    autoload :Finisher,               'rails/application/finisher'
    autoload :Railties,               'rails/engine/railties'
    autoload :RoutesReloader,         'rails/application/routes_reloader'
    ...
    # railties/lib/rails/application.rb#L348
    # Initialize the application passing the given group. By default, the
    # group is :default
    def initialize!(group=:default) #:nodoc:
      raise "Application has been already initialized." if @initialized
      run_initializers(group, self)
      @initialized = true
      self
    end
 # railties/lib/rails/engine.rb#L345
 class Engine < Railtie
    autoload :Configuration, "rails/engine/configuration"
    ...
    # railties/lib/rails/engine.rb#L519
    # Define the Rack API for this engine.
    def call(env)
      req = build_request env
      app.call req.env
    end
  • Application booting process: https://github.com/rails/rails/blob/master/railties/lib/rails/application.rb#L36

    # 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:
    #
    #   1)  require "config/boot.rb" to setup load paths
    #   2)  require railties and engines
    #   3)  Define Rails.application as "class MyApp::Application < Rails::Application"
    #   4)  Run config.before_configuration callbacks
    #   5)  Load config/environments/ENV.rb
    #   6)  Run config.before_initialize callbacks
    #   7)  Run Railtie#initializer defined by railties, engines and application.
    #       One by one, each engine sets up its load paths, routes and runs its config/initializers/* files.
    #   8)  Custom Railtie#initializers added by railties, engines and applications are executed
    #   9)  Build the middleware stack and run to_prepare callbacks
    #   10) Run config.before_eager_load and eager_load! if eager_load is true
    #   11) Run config.after_initialize callbacks

Reference

@dylanninin dylanninin added this to the Sprint 1 milestone Aug 3, 2016
@dylanninin dylanninin self-assigned this Aug 3, 2016
@dylanninin dylanninin added todo and removed todo labels Aug 3, 2016
@dylanninin
Copy link
Owner Author

A Request/Response Process

        begin
          status, headers, body = app.call(env)
          begin
            send_headers request.out, status, headers
            send_body request.out, body
          ensure
            body.close  if body.respond_to? :close
          end
        ensure
          rack_input.close
          request.finish
        end
    # railties/lib/rails/engine.rb#L519
    # Define the Rack API for this engine.
    def call(env)
      req = build_request env
      app.call req.env
    end
  • build_request env: build HTTP request enviroment . https://github.com/rails/rails/blob/master/railties/lib/rails/engine.rb#L692

      def build_request(env)
        env.merge!(env_config)
        req = ActionDispatch::Request.new env
        req.routes = routes
        req.engine_script_name = req.script_name
        req
      end
    def initialize(env)
      super
      @method            = nil
      @request_method    = nil
      @remote_ip         = nil
      @original_fullpath = nil
      @fullpath          = nil
      @ip                = nil
    end
    
    def commit_cookie_jar! # :nodoc:
    end
    
    PASS_NOT_FOUND = Class.new { # :nodoc:
      def self.action(_); self; end
      def self.call(_); [404, {'X-Cascade' => 'pass'}, []]; end
    }
    
    def controller_class
      params = path_parameters
    
      if params.key?(:controller)
        controller_param = params[:controller].underscore
        params[:action] ||= 'index'
        const_name = "#{controller_param.camelize}Controller"
        ActiveSupport::Dependencies.constantize(const_name)
      else
        PASS_NOT_FOUND
      end
    end
    
    def key?(key)
      has_header? key
    end
  • app: the app stack. https://github.com/rails/rails/blob/master/railties/lib/rails/engine.rb#L502

      ...
      delegate :middleware, :root, :paths, to: :config
      delegate :engine_name, :isolated?, to: :class
      ...
      # Returns the underlying rack application for this engine.
      def app
        @app || @app_build_lock.synchronize {
          @app ||= begin
            stack = default_middleware_stack
            config.middleware = build_middleware.merge_into(stack)
            config.middleware.build(endpoint)
          end
        }
      end
    
      # Returns the endpoint for this engine. If none is registered,
      # defaults to an ActionDispatch::Routing::RouteSet.
      def endpoint
        self.class.endpoint || routes
      end
      ...
      def default_middleware_stack #:nodoc:
        ActionDispatch::MiddlewareStack.new
      end
    # Defines the routes for this engine. If a block is given to
    # routes, it is appended to the engine.
    def routes
      @routes ||= ActionDispatch::Routing::RouteSet.new_with_config(config)
      @routes.append(&Proc.new) if block_given?
      @routes
    end
  • ActionDispatch::Routing::RouteSet: https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/routing/route_set.rb#L722

      ...
      def initialize(config = DEFAULT_CONFIG)
        self.named_routes = NamedRouteCollection.new
        self.resources_path_names = self.class.default_resources_path_names
        self.default_url_options = {}

        @config                     = config
        @append                     = []
        @prepend                    = []
        @disable_clear_and_finalize = false
        @finalized                  = false
        @env_key                    = "ROUTES_#{object_id}_SCRIPT_NAME".freeze

        @set    = Journey::Routes.new
        @router = Journey::Router.new @set
        @formatter = Journey::Formatter.new self
      end
       ...
      def call(env)
        req = make_request(env)
        req.path_info = Journey::Router::Utils.normalize_path(req.path_info)
        @router.serve(req)
      end
     ... 
    def serve(req)
        find_routes(req).each do |match, parameters, route|
          set_params  = req.path_parameters
          path_info   = req.path_info
          script_name = req.script_name

          unless route.path.anchored
            req.script_name = (script_name.to_s + match.to_s).chomp('/')
            req.path_info = match.post_match
            req.path_info = "/" + req.path_info unless req.path_info.start_with? "/"
          end

          req.path_parameters = set_params.merge parameters

          status, headers, body = route.app.serve(req)

          if 'pass' == headers['X-Cascade']
            req.script_name     = script_name
            req.path_info       = path_info
            req.path_parameters = set_params
            next
          end

          return [status, headers, body]
        end
        ...
        def custom_routes
          routes.custom_routes
        end
        ...
        def find_routes req
          routes = filter_routes(req.path_info).concat custom_routes.find_all { |r|
            r.path.match(req.path_info)
          }

          routes =
            if req.head?
              match_head_routes(routes, req)
            else
              match_routes(routes, req)
            end

          routes.sort_by!(&:precedence)

          routes.map! { |r|
            match_data  = r.path.match(req.path_info)
            path_parameters = r.defaults.dup
            match_data.names.zip(match_data.captures) { |name,val|
              path_parameters[name.to_sym] = Utils.unescape_uri(val) if val
            }
            [match_data, path_parameters, r]
          }
        end
        def initialize(set, ast, defaults, controller, default_action, modyoule, to, formatted, scope_constraints, blocks, via, options_constraints, anchor, options)
          @defaults = defaults
          @set = set

          @to                 = to
          @default_controller = controller
          @default_action     = default_action
          @ast                = ast
          @anchor             = anchor
          @via                = via
          @internal           = options[:internal]

          path_params = ast.find_all(&:symbol?).map(&:to_sym)

          options = add_wildcard_options(options, formatted, ast)

          options = normalize_options!(options, path_params, modyoule)

          split_options = constraints(options, path_params)

          constraints = scope_constraints.merge Hash[split_options[:constraints] || []]

          if options_constraints.is_a?(Hash)
            @defaults = Hash[options_constraints.find_all { |key, default|
              URL_OPTIONS.include?(key) && (String === default || Fixnum === default)
            }].merge @defaults
            @blocks = blocks
            constraints.merge! options_constraints
          else
            @blocks = blocks(options_constraints)
          end

          requirements, conditions = split_constraints path_params, constraints
          verify_regexp_requirements requirements.map(&:last).grep(Regexp)

          formats = normalize_format(formatted)

          @requirements = formats[:requirements].merge Hash[requirements]
          @conditions = Hash[conditions]
          @defaults = formats[:defaults].merge(@defaults).merge(normalize_defaults(options))

          @required_defaults = (split_options[:required_defaults] || []).map(&:first)
        end

        def make_route(name, precedence)
          route = Journey::Route.new(name,
                            application,
                            path,
                            conditions,
                            required_defaults,
                            defaults,
                            request_method,
                            precedence,
                            @internal)

          route
        end

        def application
          app(@blocks)
        end
          def app(blocks)
            if to.is_a?(Class) && to < ActionController::Metal
              Routing::RouteSet::StaticDispatcher.new to
            else
              if to.respond_to?(:call)
                Constraints.new(to, blocks, Constraints::CALL)
              elsif blocks.any?
                Constraints.new(dispatcher(defaults.key?(:controller)), blocks, Constraints::SERVE)
              else
                dispatcher(defaults.key?(:controller))
              end
            end
          end
      ...
      def draw(&block)
        clear! unless @disable_clear_and_finalize
        eval_block(block)
        finalize! unless @disable_clear_and_finalize
        nil
      end
      ...
      def eval_block(block)
        mapper = Mapper.new(self)
        if default_scope
          mapper.with_default_scope(default_scope, &block)
        else
          mapper.instance_exec(&block)
        end
      end
      private :eval_block
    # Makes the controller a Rack endpoint that runs the action in the given
    # +env+'s +action_dispatch.request.path_parameters+ key.
    def self.call(env)
      req = ActionDispatch::Request.new env
      action(req.path_parameters[:action]).call(env)
    end
    class << self; deprecate :call; end

    # Returns a Rack endpoint for the given action name.
    def self.action(name)
      if middleware_stack.any?
        middleware_stack.build(name) do |env|
          req = ActionDispatch::Request.new(env)
          res = make_response! req
          new.dispatch(name, req, res)
        end
      else
        lambda { |env|
          req = ActionDispatch::Request.new(env)
          res = make_response! req
          new.dispatch(name, req, res)
        }
      end
    end

    # Direct dispatch to the controller.  Instantiates the controller, then
    # executes the action named +name+.
    def self.dispatch(name, req, res)
      if middleware_stack.any?
        middleware_stack.build(name) { |env| new.dispatch(name, req, res) }.call req.env
      else
        new.dispatch(name, req, res)
      end
    end

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

No branches or pull requests

1 participant