public
Description: Phusion Passenger (mod_rails)
Homepage: http://www.modrails.com/
Clone URL: git://github.com/FooBarWidget/passenger.git
Click here to lend your support to: passenger and make a donation at www.pledgie.com !
Hongli Lai (Phusion) (author)
Tue Apr 29 14:53:04 -0700 2008
commit  d773c27a9a4b49a2aa5a8f0d9fba23a6d6987aba
tree    cc4949d803f7ed0bd0ffa258bbf243074cdd2741
parent  95fcd7a4c62d31cbba6e20b2ecd31ce5451f7eff
passenger / lib / passenger / application_spawner.rb
100644 330 lines (305 sloc) 10.237 kb
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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
# Phusion Passenger - http://www.modrails.com/
# Copyright (C) 2008 Phusion
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; version 2 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
require 'rubygems'
require 'socket'
require 'etc'
require 'passenger/abstract_server'
require 'passenger/application'
require 'passenger/utils'
require 'passenger/request_handler'
 
begin
  # Preload MySQL if possible. We want to preload it and we need
  # its exception classes.
  require 'mysql'
rescue LoadError
end
begin
  # Preload SQLite 3 if possible. Rails 2.0 apps use it by default.
  require 'sqlite3'
rescue LoadError
end
 
module Passenger
 
# An abstract base class for AppInitError and FrameworkInitError. This represents
# the failure when initializing something.
class InitializationError < StandardError
  # The exception that caused initialization to fail. This may be nil.
  attr_accessor :child_exception
 
  # Create a new InitializationError. +message+ is the error message,
  # and +child_exception+ is the exception that caused initialization
  # to fail.
  def initialize(message, child_exception = nil)
    super(message)
    @child_exception = child_exception
  end
end
 
# Raised when ApplicationSpawner, FrameworkSpawner or SpawnManager was unable
# spawn a Ruby on Rails application, because the application either threw an
# exception or called exit.
#
# If the +child_exception+ attribute is nil, then it means that the application
# called exit.
class AppInitError < InitializationError
end
 
# This class is capable of spawns instances of a single Ruby on Rails application.
# It does so by preloading as much of the application's code as possible, then creating
# instances of the application using what is already preloaded. This makes it spawning
# application instances very fast, except for the first spawn.
#
# Use multiple instances of ApplicationSpawner if you need to spawn multiple different
# Ruby on Rails applications.
#
# *Note*: ApplicationSpawner may only be started asynchronously with AbstractServer#start.
# Starting it synchronously with AbstractServer#start_synchronously has not been tested.
class ApplicationSpawner < AbstractServer
  include Utils
  
  # This exception means that the ApplicationSpawner server process exited unexpectedly.
  class Error < AbstractServer::ServerError
  end
  
  # The user ID of the root user.
  ROOT_UID = 0
  # The group ID of the root user.
  ROOT_GID = 0
  
  # An attribute, used internally. This should not be used outside Passenger.
  attr_accessor :time
  # The application root of this spawner.
  attr_reader :app_root
 
  # +app_root+ is the root directory of this application, i.e. the directory
  # that contains 'app/', 'public/', etc. If given an invalid directory,
  # or a directory that doesn't appear to be a Rails application root directory,
  # then an ArgumentError will be raised.
  #
  # If +lower_privilege+ is true, then ApplicationSpawner will attempt to
  # switch to the user who owns the application's <tt>config/environment.rb</tt>,
  # and to the default group of that user.
  #
  # If that user doesn't exist on the system, or if that user is root,
  # then ApplicationSpawner will attempt to switch to the username given by
  # +lowest_user+ (and to the default group of that user).
  # If +lowest_user+ doesn't exist either, or if switching user failed
  # (because the current process does not have the privilege to do so),
  # then ApplicationSpawner will continue without reporting an error.
  def initialize(app_root, lower_privilege = true, lowest_user = "nobody")
    super()
    begin
      @app_root = normalize_path(app_root)
    rescue SystemCallError => e
      raise ArgumentError, e.message
    rescue ArgumentError
      raise
    end
    @lower_privilege = lower_privilege
    @lowest_user = lowest_user
    self.time = Time.now
    assert_valid_app_root(@app_root)
    define_message_handler(:spawn_application, :handle_spawn_application)
  end
  
  # Spawn an instance of the RoR application. When successful, an Application object
  # will be returned, which represents the spawned RoR application.
  #
  # Raises:
  # - AbstractServer::ServerNotStarted: The ApplicationSpawner server hasn't already been started.
  # - ApplicationSpawner::Error: The ApplicationSpawner server exited unexpectedly.
  def spawn_application
    server.write("spawn_application")
    pid, socket_name, using_abstract_namespace = server.read
    if pid.nil?
      raise IOError, "Connection closed"
    end
    owner_pipe = server.recv_io
    return Application.new(@app_root, pid, socket_name,
      using_abstract_namespace == "true", owner_pipe)
  rescue SystemCallError, IOError, SocketError => e
    raise Error, "The application spawner server exited unexpectedly"
  end
  
  # Overrided from AbstractServer#start.
  #
  # May raise these additional exceptions:
  # - AppInitError: The Ruby on Rails application raised an exception
  # or called exit() during startup.
  # - ApplicationSpawner::Error: The ApplicationSpawner server exited unexpectedly.
  def start
    super
    begin
      status = server.read[0]
      if status == 'exception'
        child_exception = unmarshal_exception(server.read_scalar)
        stop
        raise AppInitError.new(
          "Application '#{@app_root}' raised an exception: " <<
          "#{child_exception.class} (#{child_exception.message})",
          child_exception)
      elsif status == 'exit'
        stop
        raise AppInitError.new("Application '#{@app_root}' exited during startup")
      end
    rescue IOError, SystemCallError, SocketError
      stop
      raise Error, "The application spawner server exited unexpectedly"
    end
  end
 
protected
  # Overrided method.
  def before_fork # :nodoc:
    if GC.copy_on_write_friendly?
      # Garbage collect now so that the child process doesn't have to
      # do that (to prevent making pages dirty).
      GC.start
    end
  end
  
  # Overrided method.
  def initialize_server # :nodoc:
    begin
      $0 = "Passenger ApplicationSpawner: #{@app_root}"
      Dir.chdir(@app_root)
      lower_privilege! if @lower_privilege
      preload_application
    rescue StandardError, ScriptError, NoMemoryError => e
      if ENV['TESTING_PASSENGER'] == '1'
        print_exception(self.class.to_s, e)
      end
      client.write('exception')
      client.write_scalar(marshal_exception(e))
      return
    rescue SystemExit
      client.write('exit')
      raise
    end
    client.write('success')
  end
  
private
  def lower_privilege!
    stat = File.stat("config/environment.rb")
    begin
      if !switch_to_user(stat.uid)
        switch_to_user(@lowest_user)
      end
    rescue Errno::EPERM
      # No problem if we were unable to switch user.
    end
  end
 
  def switch_to_user(user)
    begin
      if user.is_a?(String)
        pw = Etc.getpwnam(user)
        username = user
        uid = pw.uid
        gid = pw.gid
      else
        pw = Etc.getpwuid(user)
        username = pw.name
        uid = user
        gid = pw.gid
      end
    rescue
      return false
    end
    if uid == ROOT_UID
      return false
    else
      Process.groups = Process.initgroups(username, gid)
      Process::Sys.setgid(gid)
      Process::Sys.setuid(uid)
      return true
    end
  end
 
  def preload_application
    Object.const_set(:RAILS_ROOT, @app_root)
    if defined?(Rails::Initializer)
      Rails::Initializer.run(:set_load_path)
      
      # The Rails framework is loaded at the moment.
      # environment.rb may set ENV['RAILS_ENV']. So we re-initialize
      # RAILS_ENV in Rails::Initializer.load_environment.
      Rails::Initializer.class_eval do
        def load_environment_with_passenger
          if defined?(::RAILS_ENV)
            Object.send(:remove_const, :RAILS_ENV)
          end
          Object.const_set(:RAILS_ENV, (ENV['RAILS_ENV'] || 'development').dup)
          load_environment_without_passenger
        end
        
        alias_method :load_environment_without_passenger, :load_environment
        alias_method :load_environment, :load_environment_with_feature
      end
    end
    require 'config/environment'
    if ActionController::Base.page_cache_directory.blank?
      ActionController::Base.page_cache_directory = "#{RAILS_ROOT}/public"
    end
    if defined?(ActionController::Dispatcher) \
     && ActionController::Dispatcher.respond_to?(:error_file_path)
      ActionController::Dispatcher.error_file_path = "#{RAILS_ROOT}/public"
    end
    if !defined?(Dispatcher)
      require 'dispatcher'
    end
    require_dependency 'application'
    if GC.copy_on_write_friendly?
      Dir.glob('app/{models,controllers,helpers}/*.rb').each do |file|
        require_dependency normalize_path(file)
      end
    end
  end
 
  def handle_spawn_application
    # Double fork to prevent zombie processes.
    pid = fork do
      begin
        pid = fork do
          begin
            start_request_handler
          rescue SignalException => signal
            if e.message != RequestHandler::HARD_TERMINATION_SIGNAL &&
             e.message != RequestHandler::SOFT_TERMINATION_SIGNAL
              print_exception('application', e)
            end
          rescue Exception => e
            print_exception('application', e)
          ensure
            exit!
          end
        end
      rescue Exception => e
        print_exception(self.class.to_s, e)
      ensure
        exit!
      end
    end
    Process.waitpid(pid)
  end
  
  def start_request_handler
    $0 = "Rails: #{@app_root}"
    reader, writer = IO.pipe
    begin
      # Re-establish connection if a connection was established
      # in environment.rb. This prevents us from concurrently
      # accessing the same MySQL connection handle.
      if defined?(::ActiveRecord::Base) && ::ActiveRecord::Base.connected?
        ::ActiveRecord::Base.establish_connection
      end
      
      handler = RequestHandler.new(reader)
      client.write(Process.pid, handler.socket_name,
        handler.using_abstract_namespace?)
      client.send_io(writer)
      writer.close
      client.close
      handler.main_loop
    ensure
      client.close rescue nil
      writer.close rescue nil
      handler.cleanup rescue nil
    end
  end
end
 
end # module Passenger