diff options
Diffstat (limited to 'guides/source/initialization.textile')
-rw-r--r-- | guides/source/initialization.textile | 659 |
1 files changed, 659 insertions, 0 deletions
diff --git a/guides/source/initialization.textile b/guides/source/initialization.textile new file mode 100644 index 0000000000..0638bbed10 --- /dev/null +++ b/guides/source/initialization.textile @@ -0,0 +1,659 @@ +h2. The Rails Initialization Process + +This guide explains the internals of the initialization process in Rails +as of Rails 4. It is an extremely in-depth guide and recommended for advanced Rails developers. + +* Using +rails server+ +* Using Passenger + +endprologue. + +This guide goes through every method call that is +required to boot up the Ruby on Rails stack for a default Rails 4 application, explaining each part in detail along the way. For this guide, we will be focusing on how the two most common methods (+rails server+ and Passenger) boot a Rails application. + +NOTE: Paths in this guide are relative to Rails or a Rails application unless otherwise specified. + +h3. Launch! + +A Rails application is usually started with the command +rails server+. + +h4. +bin/rails+ + +The actual +rails+ command is kept in _bin/rails_: + +<ruby> +#!/usr/bin/env ruby + +if File.exists?(File.join(File.expand_path('../../..', __FILE__), '.git')) + railties_path = File.expand_path('../../lib', __FILE__) + $:.unshift(railties_path) +end +require "rails/cli" +</ruby> + +This file will first attempt to push the +railties/lib+ directory if +present, and then require +rails/cli+. + +h4. +railties/lib/rails/cli.rb+ + +This file looks like this: + +<ruby> +require 'rbconfig' +require 'rails/script_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::ScriptRailsLoader.exec_script_rails! + +require 'rails/ruby_version_check' +Signal.trap("INT") { puts; exit(1) } + +if ARGV.first == 'plugin' + ARGV.shift + require 'rails/commands/plugin_new' +else + require 'rails/commands/application' +end +</ruby> + +The +rbconfig+ file from the Ruby standard library provides us with the +RbConfig+ class which contains detailed information about the Ruby environment, including how Ruby was compiled. We can see this in use in +railties/lib/rails/script_rails_loader+. + +<ruby> +require 'pathname' + +module Rails + module ScriptRailsLoader + RUBY = File.join(*RbConfig::CONFIG.values_at("bindir", "ruby_install_name")) + RbConfig::CONFIG["EXEEXT"] + SCRIPT_RAILS = File.join('script', 'rails') + ... + + end +end +</ruby> + +The +rails/script_rails_loader+ file uses +RbConfig::Config+ to obtain the +bin_dir+ and +ruby_install_name+ values for the configuration which together form the path to the Ruby interpreter. The +RbConfig::CONFIG["EXEEXT"]+ will suffix this path with ".exe" if the script is running on Windows. This constant is used later on in +exec_script_rails!+. As for the +SCRIPT_RAILS+ constant, we'll see that when we get to the +in_rails_application?+ method. + +Back in +rails/cli+, the next line is this: + +<ruby> +Rails::ScriptRailsLoader.exec_script_rails! +</ruby> + +This method is defined in +rails/script_rails_loader+: + +<ruby> +def self.exec_script_rails! + cwd = Dir.pwd + return unless in_rails_application? || in_rails_application_subdirectory? + exec RUBY, SCRIPT_RAILS, *ARGV if in_rails_application? + Dir.chdir("..") do + # Recurse in a chdir block: if the search fails we want to be sure + # the application is generated in the original working directory. + exec_script_rails! unless cwd == Dir.pwd + end +rescue SystemCallError + # could not chdir, no problem just return +end +</ruby> + +This method will first check if the current working directory (+cwd+) is a Rails application or a subdirectory of one. This is determined by the +in_rails_application?+ method: + +<ruby> +def self.in_rails_application? + File.exists?(SCRIPT_RAILS) +end +</ruby> + +The +SCRIPT_RAILS+ constant defined earlier is used here, with +File.exists?+ checking for its presence in the current directory. If this method returns +false+ then +in_rails_application_subdirectory?+ will be used: + +<ruby> +def self.in_rails_application_subdirectory?(path = Pathname.new(Dir.pwd)) + File.exists?(File.join(path, SCRIPT_RAILS)) || !path.root? && in_rails_application_subdirectory?(path.parent) +end +</ruby> + +This climbs the directory tree until it reaches a path which contains a +script/rails+ file. If a directory containing this file is reached then this line will run: + +<ruby> +exec RUBY, SCRIPT_RAILS, *ARGV if in_rails_application? +</ruby> + +This is effectively the same as running +ruby script/rails [arguments]+, where +[arguments]+ at this point in time is simply "server". + +TIP: If you execute +script/rails+ directly from your Rails app you will +avoid executing the code that we just described. + +h4. +script/rails+ + +This file is as follows: + +<ruby> +APP_PATH = File.expand_path('../../config/application', __FILE__) +require File.expand_path('../../config/boot', __FILE__) +require 'rails/commands' +</ruby> + +The +APP_PATH+ constant will be used later in +rails/commands+. The +config/boot+ file referenced here is the +config/boot.rb+ file in our application which is responsible for loading Bundler and setting it up. + +h4. +config/boot.rb+ + ++config/boot.rb+ contains: + +<ruby> +# Set up gems listed in the Gemfile. +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) + +require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) +</ruby> + +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, +bundler/setup+ is then required. + +The gems that a Rails 4 application depends on are as follows: + +TODO: change these when the Rails 4 release is near. + +* abstract (1.0.0) +* actionmailer (4.0.0.beta) +* actionpack (4.0.0.beta) +* activemodel (4.0.0.beta) +* activerecord (4.0.0.beta) +* activesupport (4.0.0.beta) +* arel (2.0.7) +* builder (3.0.0) +* bundler (1.0.6) +* erubis (2.6.6) +* i18n (0.5.0) +* mail (2.2.12) +* mime-types (1.16) +* polyglot (0.3.1) +* rack (1.2.1) +* rack-cache (0.5.3) +* rack-mount (0.6.13) +* rack-test (0.5.6) +* rails (4.0.0.beta) +* railties (4.0.0.beta) +* rake (0.8.7) +* sqlite3-ruby (1.3.2) +* thor (0.14.6) +* treetop (1.4.9) +* tzinfo (0.3.23) + +h4. +rails/commands.rb+ + +Once +config/boot.rb+ has finished, the next file that is required is +rails/commands+ which will execute a command based on the arguments passed in. In this case, the +ARGV+ array simply contains +server+ which is extracted into the +command+ variable using these lines: + +<ruby> +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 +</ruby> + +TIP: As you can see, an empty ARGV list will make Rails show the help +snippet. + +If we used <tt>s</tt> rather than +server+, Rails will use the +aliases+ defined in the file and match them to their respective commands. With the +server+ command, Rails will run this code: + +<ruby> +when 'server' + # Change to the application's path if there is no config.ru file in current dir. + # This allows us to run script/rails server from other directories, but still get + # the main config.ru and properly set the tmp directory. + Dir.chdir(File.expand_path('../../', APP_PATH)) unless File.exists?(File.expand_path("config.ru")) + + require 'rails/commands/server' + Rails::Server.new.tap { |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 + } +</ruby> + +This file will change into the root of the directory (a path two directories back from +APP_PATH+ which points at +config/application.rb+), but only if the +config.ru+ file isn't found. This then requires +rails/commands/server+ which sets up the +Rails::Server+ class. + +<ruby> +require 'fileutils' +require 'optparse' +require 'action_dispatch' + +module Rails + class Server < ::Rack::Server +</ruby> + ++fileutils+ and +optparse+ are standard Ruby libraries which provide helper functions for working with files and parsing options. + +h4. +actionpack/lib/action_dispatch.rb+ + +Action Dispatch is the routing component of the Rails framework. Other +than the rouing itself, it adds +functionalities like routing, session, and common middlewares. + +Action Dispatch itself is also responsible for loading Active Support, Action +Pack, Active Model, and Rack. + +h4. +rails/commands/server.rb+ + +The +Rails::Server+ class is defined in this file as inheriting from +Rack::Server+. When +Rails::Server.new+ is called, this calls the +initialize+ method in +rails/commands/server.rb+: + +<ruby> +def initialize(*) + super + set_environment +end +</ruby> + +Firstly, +super+ is called which calls the +initialize+ method on +Rack::Server+. + +h4. Rack: +lib/rack/server.rb+ + ++Rack::Server+ is responsible for providing a common server interface for all Rack-based applications, which Rails is now a part of. + +The +initialize+ method in +Rack::Server+ simply sets a couple of variables: + +<ruby> +def initialize(options = nil) + @options = options + @app = options[:app] if options && options[:app] +end +</ruby> + +In this case, +options+ will be +nil+ so nothing happens in this method. + +After +super+ has finished in +Rack::Server+, we jump back to +rails/commands/server.rb+. At this point, +set_environment+ is called within the context of the +Rails::Server+ object and this method doesn't appear to do much at first glance: + +<ruby> +def set_environment + ENV["RAILS_ENV"] ||= options[:environment] +end +</ruby> + +In fact, the +options+ method here does quite a lot. This method is defined in +Rack::Server+ like this: + +<ruby> +def options + @options ||= parse_options(ARGV) +end +</ruby> + +Then +parse_options+ is defined like this: + +<ruby> +def parse_options(args) + options = default_options + + # Don't evaluate CGI ISINDEX parameters. + # http://hoohoo.ncsa.uiuc.edu/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 +</ruby> + +With the +default_options+ set to this: + +<ruby> +def default_options + { + :environment => ENV['RACK_ENV'] || "development", + :pid => nil, + :Port => 9292, + :Host => "0.0.0.0", + :AccessLog => [], + :config => "config.ru" + } +end +</ruby> + +There is no +REQUEST_METHOD+ key in +ENV+ so we can skip over that line. The next line merges in the options from +opt_parser+ which is defined plainly in +Rack::Server+ + +<ruby> +def opt_parser + Options.new +end +</ruby> + +The class *is* defined in +Rack::Server+, but is overwritten in +Rails::Server+ to take different arguments. Its +parse!+ method begins like this: + +<ruby> +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 } + ... +</ruby> + +This method will set up keys for the +options+ which Rails will then be +able to use to determine how its server should run. After +initialize+ +has finished, we jump back into +rails/server+ where +APP_PATH+ (which was +set earlier) is required. + +h4. +config/application+ + +When +require APP_PATH+ is executed, +config/application.rb+ is loaded. +This is a file exists in your app and it's free for you to change based +on your needs. Among other things, inside this file you load gems with +bundler, and create your application namespace. + +h4. +Rails::Server#start+ + +After +congif/application+ is loaded, +server.start+ is called. This method is defined like this: + +<ruby> +def start + 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 "=> Call with -d to detach" unless options[:daemonize] + trap(:INT) { exit } + puts "=> Ctrl-C to shutdown server" unless options[:daemonize] + + #Create required tmp directories if not found + %w(cache pids sessions sockets).each do |dir_to_make| + FileUtils.mkdir_p(Rails.root.join('tmp', dir_to_make)) + end + + unless options[:daemonize] + wrapped_app # touch the app so the logger is set up + + console = ActiveSupport::Logger.new($stdout) + console.formatter = Rails.logger.formatter + + Rails.logger.extend(ActiveSupport::Logger.broadcast(console)) + end + + 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 +</ruby> + +This is where the first output of the Rails initialization happens. This +method creates a trap for +INT+ signals, so if you +CTRL-C+ the server, +it will exit the process. As we can see from the code here, it will +create the +tmp/cache+, +tmp/pids+, +tmp/sessions+ and +tmp/sockets+ +directories. It then calls +wrapped_app+ which is responsible for +creating the Rack app, before creating and assigning an +instance of +ActiveSupport::Logger+. + +The +super+ method will call +Rack::Server.start+ which begins its definition like this: + +<ruby> +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 +</ruby> + +The interesting part for a Rails app is the last line, +server.run+. Here we encounter the +wrapped_app+ method again, which this time +we're going to explore more (even though it was executed before, and +thus memoized by now). + +<ruby> +@wrapped_app ||= build_app app +</ruby> + +The +app+ method here is defined like so: + +<ruby> +def app + @app ||= begin + if !::File.exist? options[:config] + abort "configuration #{options[:config]} not found" + end + + app, options = Rack::Builder.parse_file(self.options[:config], opt_parser) + self.options.merge! options + app + end +end +</ruby> + +The +options[:config]+ value defaults to +config.ru+ which contains this: + +<ruby> +# This file is used by Rack-based servers to start the application. + +require ::File.expand_path('../config/environment', __FILE__) +run <%= app_const %> +</ruby> + + +The +Rack::Builder.parse_file+ method here takes the content from this +config.ru+ file and parses it using this code: + +<ruby> +app = eval "Rack::Builder.new {( " <plus> cfgfile <plus> "\n )}.to_app", + TOPLEVEL_BINDING, config +</ruby> + +The +initialize+ method of +Rack::Builder+ will take the block here and execute it within an instance of +Rack::Builder+. This is where the majority of the initialization process of Rails happens. The +require+ line for +config/environment.rb+ in +config.ru+ is the first to run: + +<ruby> +require ::File.expand_path('../config/environment', __FILE__) +</ruby> + +h4. +config/environment.rb+ + +This file is the common file required by +config.ru+ (+rails server+) and Passenger. This is where these two ways to run the server meet; everything before this point has been Rack and Rails setup. + +This file begins with requiring +config/application.rb+. + +h4. +config/application.rb+ + +This file requires +config/boot.rb+, but only if it hasn't been required before, which would be the case in +rails server+ but *wouldn't* be the case with Passenger. + +Then the fun begins! + +h3. Loading Rails + +The next line in +config/application.rb+ is: + +<ruby> +require 'rails/all' +</ruby> + +h4. +railties/lib/rails/all.rb+ + +This file is responsible for requiring all the individual parts of Rails like so: + +<ruby> +require "rails" + +%w( + active_record + action_controller + action_mailer + rails/test_unit + sprockets/rails +).each do |framework| + begin + require "#{framework}/railtie" + rescue LoadError + end +end +</ruby> + +This is where all the Rails frameworks are loaded and thus made +available to the application. We wont go into detail of what happens +inside each of those frameworks, but you're encouraged to try and +explore them on your own. + +For now, just keep in mind that common functionality like Rails engines, +I18n and Rails configuration is all bein defined here. + +h4. Back to +config/environment.rb+ + +When +config/application.rb+ has finished loading Rails, and defined +your application namespace, you go back to +config/environment.rb+, +where your application is initialized. For example, if you application was called ++Blog+, here you would find +Blog::Application.initialize!+, which is +defined in +rails/application.rb+ + +h4. +railties/lib/rails/application.rb+ + +The +initialize!+ method looks like this: + +<ruby> +def initialize!(group=:default) #:nodoc: + raise "Application has been already initialized." if @initialized + run_initializers(group, self) + @initialized = true + self +end +</ruby> + +As you can see, you can only initialize an app once. This is also where the initializers are run. + +TODO: review this + +The initializers code itself is tricky. What Rails is doing here is it +traverses all the class ancestors looking for an +initializers+ method, +sorting them and running them. For example, the +Engine+ class will make +all the engines available by providing the +initializers+ method. + +After this is done we go back to +Rack::Server+ + +h4. Rack: lib/rack/server.rb + +Last time we left when the +app+ method was being defined: + +<ruby> +def app + @app ||= begin + if !::File.exist? options[:config] + abort "configuration #{options[:config]} not found" + end + + app, options = Rack::Builder.parse_file(self.options[:config], opt_parser) + self.options.merge! options + app + end +end +</ruby> + +At this point +app+ is the Rails app itself (a middleware), and what +happens next is Rack will call all the provided middlewares: + +<ruby> +def build_app(app) + middleware[options[:environment]].reverse_each do |middleware| + middleware = middleware.call(self) if middleware.respond_to?(:call) + next unless middleware + klass = middleware.shift + app = klass.new(app, *middleware) + end + app +end +</ruby> + +Remember, +build_app+ was called (by wrapped_app) in the last line of +Server#start+. +Here's how it looked like when we left: + +<ruby> +server.run wrapped_app, options, &blk +</ruby> + +At this point, the implementation of +server.run+ will depend on the +server you're using. For example, if you were using Mongrel, here's what +the +run+ method would look like: + +<ruby> +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 +</ruby> + +We wont dig into the server configuration itself, but this is +the last piece of our journey in the Rails initialization process. + +This high level overview will help you understand when you code is +executed and how, and overall become a better Rails developer. If you +still want to know more, the Rails source code itself is probably the +best place to go next. |