diff --git a/.gitignore b/.gitignore index 7bdf1deb..c92d76d1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,23 +1,26 @@ references scgi_demo -test*.rb +/test*.rb *.o *.so +*.bundle *.pyc ext/apache2/*.la ext/apache2/*.lo ext/apache2/*.slo ext/apache2/.libs ext/apache2/ApplicationPoolServerExecutable -ext/passenger/Makefile -ext/boost/src/*.a +ext/phusion_passenger/Makefile +ext/*.a doc/rdoc doc/cxxapi doc/*.html +test/test.log test/Apache2ModuleTests test/config.yml test/coverage +test/oxt/oxt_test_main test/stub/railsapp/app/controllers/bar_controller.rb test/stub/*/log/* test/stub/apache2/*.log diff --git a/DEVELOPERS.TXT b/DEVELOPERS.TXT index 1d927de7..34c54d7c 100644 --- a/DEVELOPERS.TXT +++ b/DEVELOPERS.TXT @@ -5,7 +5,7 @@ The tests need the following software installed: * All the usual Phusion Passenger dependencies. -* Ruby on Rails >= 2.0.1 +* Ruby on Rails 2.0.1 (*exactly* 2.0.1) * rspec >= 1.1.2 * mime-types >= 1.15 @@ -27,9 +27,9 @@ Run the following command to compile everything: == Directory structure The most important directories are: -[ lib/passenger ] +[ lib/phusion_passenger ] The source code for the spawn server, which is written in Ruby. -[ ext/passenger ] +[ ext/phusion_passenger ] Native extensions for Ruby, used by the spawn server. [ ext/apache2 ] The Phusion Passenger Apache 2 module (mod_passenger). @@ -48,7 +48,13 @@ Less important directories: [ lib/rake ] Rake tasks. [ ext/boost ] - A vendor copy of the Boost C++ library (www.boost.org). + A stripped-down and customized version of the Boost C++ library + (www.boost.org). +[ ext/oxt ] + The "OS eXtensions for boosT" library, which provides various important + functionality necessary for writing robust server software. It provides + things like support for interruptable system calls and portable backtraces + for C++. Boost was modified to make use of the functionality provided by OXT. [ benchmark ] Benchmark tools. [ misc ] diff --git a/LICENSE b/LICENSE index 3c00af22..5547e9b2 100644 --- a/LICENSE +++ b/LICENSE @@ -2,7 +2,7 @@ Phusion Passenger is licensed under the GNU General Public License (GPL) version 2, and *only* version 2 (i.e. not version 3 or any later versions). In addition to the GNU General Public License v2 licensing terms, we explicitly -grand you the permission to run any application on top of Phusion Passenger, +grant you the permission to run any application on top of Phusion Passenger, regardless of the application's own licensing terms. The application will not be bound to the terms of the GPL in any way. That is, the GPL only applies to Phusion Passenger itself, and not to applications that are run through Phusion diff --git a/NEWS b/NEWS new file mode 100644 index 00000000..e69de29b diff --git a/Rakefile b/Rakefile index e0ebbd6e..80baae45 100644 --- a/Rakefile +++ b/Rakefile @@ -22,33 +22,30 @@ require 'rake/rdoctask' require 'rake/gempackagetask' require 'rake/extensions' require 'rake/cplusplus' -require 'passenger/platform_info' +require 'phusion_passenger/platform_info' verbose true ##### Configuration # Don't forget to edit Configuration.h too -PACKAGE_VERSION = "2.0.6" +PACKAGE_VERSION = "2.1.1" OPTIMIZE = ["yes", "on", "true"].include?(ENV['OPTIMIZE']) -include PlatformInfo -APXS2.nil? and raise "Could not find 'apxs' or 'apxs2'." -APACHE2CTL.nil? and raise "Could not find 'apachectl' or 'apache2ctl'." -HTTPD.nil? and raise "Could not find the Apache web server binary." -APR_FLAGS.nil? and raise "Could not find Apache Portable Runtime (APR)." -APU_FLAGS.nil? and raise "Could not find Apache Portable Runtime Utility (APU)." +PlatformInfo.apxs2.nil? and raise "Could not find 'apxs' or 'apxs2'." +PlatformInfo.apache2ctl.nil? and raise "Could not find 'apachectl' or 'apache2ctl'." +PlatformInfo.httpd.nil? and raise "Could not find the Apache web server binary." CXX = "g++" -THREADING_FLAGS = "-D_REENTRANT" +LIBEXT = PlatformInfo.library_extension +# _GLIBCPP__PTHREADS is for fixing Boost compilation on OpenBSD. if OPTIMIZE - OPTIMIZATION_FLAGS = "-O2 -DNDEBUG" + OPTIMIZATION_FLAGS = "-O2 -DBOOST_DISABLE_ASSERTS" else - OPTIMIZATION_FLAGS = "-g -DPASSENGER_DEBUG" + OPTIMIZATION_FLAGS = "-g -DPASSENGER_DEBUG -DBOOST_DISABLE_ASSERTS" end -CXXFLAGS = "#{THREADING_FLAGS} #{OPTIMIZATION_FLAGS} -Wall -I/usr/local/include #{MULTI_ARCH_FLAGS}" -LDFLAGS = "" - +CXXFLAGS = "-Wall #{OPTIMIZATION_FLAGS}" +EXTRA_LDFLAGS = "" #### Default tasks @@ -56,6 +53,7 @@ desc "Build everything" task :default => [ :native_support, :apache2, + 'test/oxt/oxt_test_main', 'test/Apache2ModuleTests', 'benchmark/DummyRequestHandler' ] @@ -69,7 +67,7 @@ task :clobber ##### Ruby C extension -subdir 'ext/passenger' do +subdir 'ext/phusion_passenger' do task :native_support => ["native_support.#{LIBEXT}"] file 'Makefile' => 'extconf.rb' do @@ -87,40 +85,53 @@ subdir 'ext/passenger' do end -##### boost::thread static library +##### boost::thread and OXT static library -subdir 'ext/boost/src' do - file 'libboost_thread.a' => Dir['*.cpp'] + Dir['pthread/*.cpp'] do - flags = "#{OPTIMIZATION_FLAGS} -fPIC -I../.. #{THREADING_FLAGS} -DNDEBUG #{MULTI_ARCH_FLAGS}" +file 'ext/libboost_oxt.a' => + Dir['ext/boost/src/*.cpp'] + + Dir['ext/boost/src/pthread/*.cpp'] + + Dir['ext/oxt/*.cpp'] + + Dir['ext/oxt/*.hpp'] + + Dir['ext/oxt/detail/*.hpp'] do + Dir.chdir('ext/boost/src') do + puts "### In ext/boost/src:" + flags = "-I../.. #{CXXFLAGS} #{PlatformInfo.apache2_module_cflags}" compile_cxx "*.cpp", flags # NOTE: 'compile_cxx "pthread/*.cpp", flags' doesn't work on some systems, # so we do this instead. Dir['pthread/*.cpp'].each do |file| compile_cxx file, flags end - create_static_library "libboost_thread.a", "*.o" end - - task :clean do - sh "rm -f libboost_thread.a *.o" + Dir.chdir('ext/oxt') do + puts "### In ext/oxt:" + Dir['*.cpp'].each do |file| + compile_cxx file, "-I.. #{CXXFLAGS} #{PlatformInfo.apache2_module_cflags}" + end end + create_static_library "ext/libboost_oxt.a", "ext/boost/src/*.o ext/oxt/*.o" +end + +task :clean do + sh "rm -f ext/libboost_oxt.a ext/boost/src/*.o ext/oxt/*.o" end ##### Apache module class APACHE2 - CXXFLAGS = "-I.. -fPIC #{OPTIMIZATION_FLAGS} #{APR_FLAGS} #{APU_FLAGS} #{APXS2_FLAGS} #{::CXXFLAGS}" + CXXFLAGS = "-I.. #{CXXFLAGS} #{PlatformInfo.apache2_module_cflags}" OBJECTS = { 'Configuration.o' => %w(Configuration.cpp Configuration.h), 'Bucket.o' => %w(Bucket.cpp Bucket.h), 'Hooks.o' => %w(Hooks.cpp Hooks.h Configuration.h ApplicationPool.h ApplicationPoolServer.h SpawnManager.h Exceptions.h Application.h MessageChannel.h - System.h Utils.h), - 'System.o' => %w(System.cpp System.h), + PoolOptions.h Utils.h DirectoryMapper.h FileChecker.h), 'Utils.o' => %w(Utils.cpp Utils.h), - 'Logging.o' => %w(Logging.cpp Logging.h) + 'Logging.o' => %w(Logging.cpp Logging.h), + 'SystemTime.o' => %w(SystemTime.cpp SystemTime.h), + 'CachedFileStat.o' => %w(CachedFileStat.cpp CachedFileStat.h) } end @@ -131,7 +142,7 @@ subdir 'ext/apache2' do task :apache2 => ['mod_passenger.so', 'ApplicationPoolServerExecutable', :native_support] file 'mod_passenger.so' => [ - '../boost/src/libboost_thread.a', + '../libboost_oxt.a', 'mod_passenger.o' ] + APACHE2::OBJECTS.keys do # apxs totally sucks. We couldn't get it working correctly @@ -140,27 +151,35 @@ subdir 'ext/apache2' do # Apache module ourselves. # # Oh, and libtool sucks too. Do we even need it anymore in 2008? - linkflags = "#{LDFLAGS} #{MULTI_ARCH_FLAGS}" - linkflags << " -lstdc++ -lpthread ../boost/src/libboost_thread.a #{APR_LIBS}" + linkflags = "../libboost_oxt.a " + linkflags << "#{PlatformInfo.apache2_module_cflags} " + linkflags << "#{PlatformInfo.apache2_module_ldflags} #{EXTRA_LDFLAGS} -lstdc++" create_shared_library 'mod_passenger.so', APACHE2::OBJECTS.keys.join(' ') << ' mod_passenger.o', linkflags end file 'ApplicationPoolServerExecutable' => [ - '../boost/src/libboost_thread.a', + '../libboost_oxt.a', 'ApplicationPoolServerExecutable.cpp', 'ApplicationPool.h', + 'Application.h', 'StandardApplicationPool.h', + 'FileChecker.h', 'MessageChannel.h', 'SpawnManager.h', - 'System.o', + 'PoolOptions.h', 'Utils.o', - 'Logging.o' + 'Logging.o', + 'SystemTime.o', + 'CachedFileStat.o' ] do create_executable "ApplicationPoolServerExecutable", - 'ApplicationPoolServerExecutable.cpp System.o Utils.o Logging.o', - "-I.. #{CXXFLAGS} #{LDFLAGS} -DPASSENGER_DEBUG ../boost/src/libboost_thread.a -lpthread" + 'ApplicationPoolServerExecutable.cpp Utils.o Logging.o ' << + 'SystemTime.o CachedFileStat.o', + "-I.. #{CXXFLAGS} #{PlatformInfo.portability_cflags} " << + "../libboost_oxt.a " << + "#{PlatformInfo.portability_ldflags} #{EXTRA_LDFLAGS}" end file 'mod_passenger.o' => ['mod_passenger.c'] do @@ -187,44 +206,65 @@ end ##### Unit tests class TEST - CXXFLAGS = "#{::CXXFLAGS} -Isupport -DTESTING_SPAWN_MANAGER -DTESTING_APPLICATION_POOL " - AP2_FLAGS = "-I../ext/apache2 -I../ext #{APR_FLAGS} #{APU_FLAGS}" - + CXXFLAGS = "#{::CXXFLAGS} -DTESTING_SPAWN_MANAGER -DTESTING_APPLICATION_POOL #{PlatformInfo.portability_cflags}" + + AP2_FLAGS = "-I../ext/apache2 -I../ext -Isupport #{PlatformInfo.apr_flags} #{PlatformInfo.apu_flags}" AP2_OBJECTS = { 'CxxTestMain.o' => %w(CxxTestMain.cpp), 'MessageChannelTest.o' => %w(MessageChannelTest.cpp - ../ext/apache2/MessageChannel.h - ../ext/apache2/System.h), + ../ext/apache2/MessageChannel.h), 'SpawnManagerTest.o' => %w(SpawnManagerTest.cpp ../ext/apache2/SpawnManager.h + ../ext/apache2/PoolOptions.h ../ext/apache2/Application.h - ../ext/apache2/MessageChannel.h - ../ext/apache2/System.h), + ../ext/apache2/MessageChannel.h), 'ApplicationPoolServerTest.o' => %w(ApplicationPoolServerTest.cpp ../ext/apache2/ApplicationPoolServer.h - ../ext/apache2/MessageChannel.h - ../ext/apache2/System.h), + ../ext/apache2/PoolOptions.h + ../ext/apache2/MessageChannel.h), 'ApplicationPoolServer_ApplicationPoolTest.o' => %w(ApplicationPoolServer_ApplicationPoolTest.cpp ApplicationPoolTest.cpp ../ext/apache2/ApplicationPoolServer.h ../ext/apache2/ApplicationPool.h ../ext/apache2/SpawnManager.h + ../ext/apache2/PoolOptions.h ../ext/apache2/Application.h - ../ext/apache2/MessageChannel.h - ../ext/apache2/System.h), + ../ext/apache2/MessageChannel.h), 'StandardApplicationPoolTest.o' => %w(StandardApplicationPoolTest.cpp ApplicationPoolTest.cpp ../ext/apache2/ApplicationPool.h ../ext/apache2/StandardApplicationPool.h ../ext/apache2/SpawnManager.h - ../ext/apache2/Application.h), + ../ext/apache2/PoolOptions.h + ../ext/apache2/Application.h + ../ext/apache2/FileChecker.h), + 'PoolOptionsTest.o' => %w(PoolOptionsTest.cpp ../ext/apache2/PoolOptions.h), + 'FileCheckerTest.o' => %w(FileCheckerTest.cpp ../ext/apache2/FileChecker.h), + 'SystemTimeTest.o' => %w(SystemTimeTest.cpp + ../ext/apache2/SystemTime.h + ../ext/apache2/SystemTime.cpp), + 'CachedFileStatTest.o' => %w(CachedFileStatTest.cpp + ../ext/apache2/CachedFileStat.h + ../ext/apache2/CachedFileStat.cpp), 'UtilsTest.o' => %w(UtilsTest.cpp ../ext/apache2/Utils.h) } + + OXT_FLAGS = "-I../../ext -I../support" + OXT_OBJECTS = { + 'oxt_test_main.o' => %w(oxt_test_main.cpp), + 'backtrace_test.o' => %w(backtrace_test.cpp), + 'syscall_interruption_test.o' => %w(syscall_interruption_test.cpp) + } end subdir 'test' do desc "Run all unit tests (but not integration tests)" - task :test => [:'test:apache2', :'test:ruby', :'test:integration'] + task :test => [:'test:oxt', :'test:apache2', :'test:ruby', :'test:integration'] + + desc "Run unit tests for the OXT library" + task 'test:oxt' => 'oxt/oxt_test_main' do + sh "./oxt/oxt_test_main" + end desc "Run unit tests for the Apache 2 module" task 'test:apache2' => [ @@ -261,19 +301,41 @@ subdir 'test' do task 'test:integration' => [:apache2, :native_support] do sh "spec -c -f s integration_tests.rb" end + + file 'oxt/oxt_test_main' => TEST::OXT_OBJECTS.keys.map{ |x| "oxt/#{x}" } + + ['../ext/libboost_oxt.a'] do + Dir.chdir('oxt') do + objects = TEST::OXT_OBJECTS.keys.join(' ') + create_executable "oxt_test_main", objects, + "../../ext/libboost_oxt.a " << + "#{PlatformInfo.portability_ldflags} #{EXTRA_LDFLAGS}" + end + end + + TEST::OXT_OBJECTS.each_pair do |target, sources| + file "oxt/#{target}" => sources.map{ |x| "oxt/#{x}" } do + Dir.chdir('oxt') do + puts "### In test/oxt:" + compile_cxx sources[0], "#{TEST::OXT_FLAGS} #{TEST::CXXFLAGS}" + end + end + end file 'Apache2ModuleTests' => TEST::AP2_OBJECTS.keys + - ['../ext/boost/src/libboost_thread.a', - '../ext/apache2/System.o', + ['../ext/libboost_oxt.a', '../ext/apache2/Utils.o', - '../ext/apache2/Logging.o'] do + '../ext/apache2/Logging.o', + '../ext/apache2/SystemTime.o', + '../ext/apache2/CachedFileStat.o'] do objects = TEST::AP2_OBJECTS.keys.join(' ') << - " ../ext/apache2/System.o" << " ../ext/apache2/Utils.o" << - " ../ext/apache2/Logging.o" + " ../ext/apache2/Logging.o" << + " ../ext/apache2/SystemTime.o " << + " ../ext/apache2/CachedFileStat.o" create_executable "Apache2ModuleTests", objects, - "#{LDFLAGS} #{APR_LIBS} #{MULTI_ARCH_FLAGS} " << - "../ext/boost/src/libboost_thread.a -lpthread" + "#{PlatformInfo.apache2_module_ldflags} #{EXTRA_LDFLAGS} " << + "../ext/libboost_oxt.a " << + "-lpthread" end TEST::AP2_OBJECTS.each_pair do |target, sources| @@ -295,41 +357,7 @@ subdir 'test' do end task :clean do - sh "rm -f Apache2ModuleTests *.o" - end -end - - -##### Benchmarks - -subdir 'benchmark' do - file 'DummyRequestHandler' => ['DummyRequestHandler.cpp', - '../ext/apache2/MessageChannel.h', - '../ext/apache2/System.o', - '../ext/boost/src/libboost_thread.a'] do - create_executable "DummyRequestHandler", "DummyRequestHandler.cpp", - "-I../ext -I../ext/apache2 #{CXXFLAGS} #{LDFLAGS} " << - "../ext/apache2/System.o " << - "../ext/boost/src/libboost_thread.a -lpthread" - end - - file 'ApplicationPool' => ['ApplicationPool.cpp', - '../ext/apache2/StandardApplicationPool.h', - '../ext/apache2/ApplicationPoolServerExecutable', - '../ext/apache2/System.o', - '../ext/apache2/Logging.o', - '../ext/apache2/Utils.o', - '../ext/boost/src/libboost_thread.a', - :native_support] do - create_executable "ApplicationPool", "ApplicationPool.cpp", - "-I../ext -I../ext/apache2 #{CXXFLAGS} #{LDFLAGS} " << - "../ext/apache2/System.o ../ext/apache2/Logging.o " << - "../ext/apache2/Utils.o " << - "../ext/boost/src/libboost_thread.a -lpthread" - end - - task :clean do - sh "rm -f DummyRequestHandler ApplicationPool" + sh "rm -f oxt/oxt_test_main oxt/*.o Apache2ModuleTests *.o" end end @@ -337,16 +365,29 @@ end ##### Documentation subdir 'doc' do - ASCIIDOC = "asciidoc -a toc -a numbered -a toclevels=3 -a icons" + ASCIIDOC = 'asciidoc' + ASCIIDOC_FLAGS = "-a toc -a numbered -a toclevels=3 -a icons" ASCII_DOCS = ['Security of user switching support', 'Users guide', 'Architectural overview'] + DOXYGEN = 'doxygen' + desc "Generate all documentation" - task :doc => [:rdoc, :doxygen] + ASCII_DOCS.map{ |x| "#{x}.html" } + task :doc => [:rdoc] + if PlatformInfo.find_command(DOXYGEN) + task :doc => :doxygen + end + + task :doc => ASCII_DOCS.map{ |x| "#{x}.html" } + ASCII_DOCS.each do |name| file "#{name}.html" => ["#{name}.txt"] do - sh "#{ASCIIDOC} '#{name}.txt'" + if PlatformInfo.find_command(ASCIIDOC) + sh "#{ASCIIDOC} #{ASCIIDOC_FLAGS} '#{name}.txt'" + else + sh "echo 'asciidoc required to build docs' > '#{name}.html'" + end end end @@ -375,7 +416,10 @@ Rake::RDocTask.new(:clobber_rdoc => "rdoc:clobber", :rerdoc => "rdoc:force") do rd.main = "README" rd.rdoc_dir = "doc/rdoc" rd.rdoc_files.include("README", "DEVELOPERS.TXT", - "lib/passenger/*.rb", "lib/rake/extensions.rb", "ext/passenger/*.c") + "lib/phusion_passenger/*.rb", + "lib/phusion_passenger/*/*.rb", + "lib/rake/extensions.rb", + "ext/phusion_passenger/*.c") rd.template = "./doc/template/horo" rd.title = "Passenger Ruby API" rd.options << "-S" << "-N" << "-p" << "-H" @@ -394,20 +438,20 @@ spec = Gem::Specification.new do |s| s.author = "Phusion - http://www.phusion.nl/" s.email = "info@phusion.nl" s.requirements << "fastthread" << "Apache 2 with development headers" - s.require_path = ["lib", "ext"] + s.require_paths = ["lib", "ext"] s.add_dependency 'rake', '>= 0.8.1' s.add_dependency 'fastthread', '>= 1.0.1' - s.add_dependency 'rack', '>= 0.1.0' - s.extensions << 'ext/passenger/extconf.rb' + s.extensions << 'ext/phusion_passenger/extconf.rb' s.files = FileList[ 'Rakefile', 'README', 'DEVELOPERS.TXT', 'LICENSE', 'INSTALL', + 'NEWS', 'lib/**/*.rb', 'lib/**/*.py', - 'lib/passenger/templates/*', + 'lib/phusion_passenger/templates/*', 'bin/*', 'doc/*', @@ -427,11 +471,16 @@ spec = Gem::Specification.new do |s| 'ext/apache2/*.{cpp,h,c,TXT}', 'ext/boost/*.{hpp,TXT}', 'ext/boost/**/*.{hpp,cpp,pl,inl,ipp}', - 'ext/passenger/*.{c,rb}', + 'ext/oxt/*.hpp', + 'ext/oxt/*.cpp', + 'ext/oxt/detail/*.hpp', + 'ext/phusion_passenger/*.{c,rb}', 'benchmark/*.{cpp,rb}', 'misc/*', + 'vendor/**/*', 'test/*.{rb,cpp,example}', 'test/support/*', + 'test/oxt/*.cpp', 'test/ruby/*', 'test/ruby/*/*', 'test/stub/*', @@ -488,20 +537,20 @@ task :fakeroot => [:apache2, :native_support, :doc] do libdir = "#{fakeroot}/usr/lib/ruby/#{CONFIG['ruby_version']}" extdir = "#{libdir}/#{CONFIG['arch']}" bindir = "#{fakeroot}/usr/bin" - docdir = "#{fakeroot}/usr/share/doc/passenger" - libexecdir = "#{fakeroot}/usr/lib/passenger" + docdir = "#{fakeroot}/usr/share/doc/phusion_passenger" + libexecdir = "#{fakeroot}/usr/lib/phusion_passenger" sh "rm -rf #{fakeroot}" sh "mkdir -p #{fakeroot}" sh "mkdir -p #{libdir}" - sh "cp -R lib/passenger #{libdir}/" + sh "cp -R lib/phusion_passenger #{libdir}/" sh "mkdir -p #{fakeroot}/etc" sh "echo -n '#{PACKAGE_VERSION}' > #{fakeroot}/etc/passenger_version.txt" - sh "mkdir -p #{extdir}/passenger" - sh "cp -R ext/passenger/*.#{LIBEXT} #{extdir}/passenger/" + sh "mkdir -p #{extdir}/phusion_passenger" + sh "cp -R ext/phusion_passenger/*.#{LIBEXT} #{extdir}/phusion_passenger/" sh "mkdir -p #{bindir}" sh "cp bin/* #{bindir}/" @@ -526,9 +575,14 @@ task 'package:debian' => :fakeroot do end fakeroot = "pkg/fakeroot" - arch = `uname -m`.strip - if arch =~ /^i.86$/ - arch = "i386" + raw_arch = `uname -m`.strip + arch = case raw_arch + when /^i.86$/ + "i386" + when /^x86_64/ + "amd64" + else + raw_arch end sh "sed -i 's/Version: .*/Version: #{PACKAGE_VERSION}/' debian/control" @@ -556,10 +610,11 @@ task :sloccount do end sh "sloccount", *Dir[ "#{tmpdir}/*", - "lib/passenger/*", + "lib/phusion_passenger/*", "lib/rake/{cplusplus,extensions}.rb", "ext/apache2", - "ext/passenger/*.c", + "ext/oxt", + "ext/phusion_passenger/*.c", "test/*.{cpp,rb}", "test/support/*.rb", "test/stub/*.rb", diff --git a/benchmark/dispatcher.rb b/benchmark/dispatcher.rb index 1b079e9a..3157db28 100755 --- a/benchmark/dispatcher.rb +++ b/benchmark/dispatcher.rb @@ -2,16 +2,15 @@ # Benchmark raw speed of the Rails dispatcher. PASSENGER_ROOT = File.expand_path("#{File.dirname(__FILE__)}/..") $LOAD_PATH << "#{PASSENGER_ROOT}/lib" +$LOAD_PATH << "#{PASSENGER_ROOT}/ext" ENV["RAILS_ENV"] = "production" require 'yaml' require 'benchmark' -require 'passenger/request_handler' require 'config/environment' +require 'passenger/railz/cgi_fixed' require 'dispatcher' -include Passenger - class OutputChannel def write(data) # Black hole @@ -25,7 +24,7 @@ def start(iterations) milestone = 1 if milestone == 0 result = Benchmark.measure do iterations.times do |i| - cgi = CGIFixed.new(headers, output, STDOUT) + cgi = PhusionPassenger::Railz::CGIFixed.new(headers, output, output) ::Dispatcher.dispatch(cgi, ::ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS, cgi.stdoutput) @@ -39,8 +38,5 @@ def start(iterations) end puts "Benchmark started." -t = Thread.new do - #start(ARGV[0] ? ARGV[0].to_i : 1000) -end -start(ARGV[0] ? ARGV[0].to_i : 1000) -t.join +start(ARGV[0] ? ARGV[0].to_i : 2000) + diff --git a/bin/passenger-install-apache2-module b/bin/passenger-install-apache2-module index c6eb2307..9fa3f9db 100755 --- a/bin/passenger-install-apache2-module +++ b/bin/passenger-install-apache2-module @@ -27,13 +27,14 @@ $LOAD_PATH.unshift("#{PASSENGER_ROOT}/ext") # the start (i.e. not via 'su' or 'sudo'). ENV["PATH"] += ":/usr/sbin:/sbin:/usr/local/sbin" -require 'passenger/platform_info' -require 'passenger/dependencies' -require 'passenger/console_text_template' +require 'optparse' +require 'phusion_passenger/platform_info' +require 'phusion_passenger/dependencies' +require 'phusion_passenger/console_text_template' include PlatformInfo class Installer - include Passenger + include PhusionPassenger PASSENGER_WEBSITE = "http://www.modrails.com/" PHUSION_WEBSITE = "www.phusion.nl" @@ -45,14 +46,25 @@ class Installer Dependencies::RubyGems, Dependencies::Rake, Dependencies::Apache2, - Dependencies::Apache2_DevHeaders, - Dependencies::APR_DevHeaders, - Dependencies::APU_DevHeaders, - Dependencies::FastThread, - Dependencies::Rack + Dependencies::Apache2_DevHeaders ] + if Dependencies.fastthread_required? + REQUIRED_DEPENDENCIES << Dependencies::FastThread + end + # Some broken servers don't have apr-config or apu-config installed. + # Nevertheless, it is possible to compile Apache modules if Apache + # was configured with --included-apr. So here we check whether + # apr-config and apu-config are available. If they're not available, + # then we only register them as required dependency if no Apache + # module can be compiled without their presence. + if (PlatformInfo.apr_config && PlatformInfo.apu_config) || + PlatformInfo.apr_config_needed_for_building_apache_modules? + REQUIRED_DEPENDENCIES << Dependencies::APR_DevHeaders + REQUIRED_DEPENDENCIES << Dependencies::APU_DevHeaders + end - def start + def start(automatic = false) + @auto = automatic if natively_packaged? check_dependencies || exit(1) show_apache2_config_snippets @@ -147,7 +159,7 @@ private # ... # Server compiled with.... # -D APACHE_MPM_DIR="server/mpm/prefork" - output = `#{HTTPD} -V` + output = `#{PlatformInfo.httpd} -V` output =~ /^Server MPM: +(.*)$/ if $1 mpm = $1.downcase @@ -188,11 +200,11 @@ private color_puts 'Compiling and installing Apache 2 module...' puts "cd #{PASSENGER_ROOT}" if ENV['TRACE'] - puts "#{RUBY} -S rake --trace clean apache2" - return system(RUBY, "-S", "rake", "--trace", "clean", "apache2") + puts "#{RUBY} -S #{RAKE} --trace clean apache2" + return system(RUBY, "-S", RAKE, "--trace", "clean", "apache2") else - puts "#{RUBY} -S rake clean apache2" - return system(RUBY, "-S", "rake", "clean", "apache2") + puts "#{RUBY} -S #{RAKE} clean apache2" + return system(RUBY, "-S", RAKE, "clean", "apache2") end end @@ -200,7 +212,7 @@ private puts line if natively_packaged? - module_location = "#{PASSENGER_ROOT}/lib/passenger/mod_passenger.so" + module_location = "#{PASSENGER_ROOT}/lib/phusion_passenger/mod_passenger.so" else module_location = "#{PASSENGER_ROOT}/ext/apache2/mod_passenger.so" end @@ -251,6 +263,7 @@ private end def wait(timeout = nil) + return if @auto begin if timeout require 'timeout' unless defined?(Timeout) @@ -305,7 +318,7 @@ private def self.determine_users_guide if natively_packaged? - return "/usr/share/doc/passenger/Users guide.html" + return "/usr/share/doc/phusion_passenger/Users guide.html" else return "#{PASSENGER_ROOT}/doc/Users guide.html" end @@ -315,4 +328,25 @@ private USERS_GUIDE = determine_users_guide end -Installer.new.start +options = {} +parser = OptionParser.new do |opts| + opts.banner = "Usage: passenger-install-apache2-module [options]" + opts.separator "" + + opts.separator "Options:" + opts.on("-a", "--auto", String, "Automatically build the Apache module,\n" << + "#{' ' * 37}without interactively asking for user\n" << + "#{' ' * 37}input.") do + options[:auto] = true + end +end +begin + parser.parse! +rescue OptionParser::ParseError => e + puts e + puts + puts "Please see '--help' for valid options." + exit 1 +end + +Installer.new.start(options[:auto]) diff --git a/bin/passenger-memory-stats b/bin/passenger-memory-stats index 461ac814..d3398c54 100755 --- a/bin/passenger-memory-stats +++ b/bin/passenger-memory-stats @@ -18,7 +18,14 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. $LOAD_PATH.unshift("#{File.dirname(__FILE__)}/../lib") -require 'passenger/platform_info' +require 'phusion_passenger/platform_info' + +# ANSI color codes +RESET = "\e[0m" +BOLD = "\e[1m" +WHITE = "\e[37m" +YELLOW = "\e[33m" +BLUE_BG = "\e[44m" # Container for tabular data. class Table @@ -64,14 +71,22 @@ class Table left_bar_size = free_space / 2 right_bar_size = free_space - left_bar_size end - result = "#{"-" * left_bar_size} #{title} #{"-" * right_bar_size}\n" - result << header + result = "#{BLUE_BG}#{BOLD}#{YELLOW}" + result << "#{"-" * left_bar_size} #{title} #{"-" * right_bar_size}\n" + if !@rows.empty? + result << WHITE + result << header + end else result = header.dup end - result << ("-" * header.size) << "\n" - @rows.each do |row| - result << sprintf(format_string, *row).rstrip << "\n" + if @rows.empty? + result << RESET + else + result << ("-" * header.size) << "#{RESET}\n" + @rows.each do |row| + result << sprintf(format_string, *row).rstrip << "\n" + end end result end @@ -109,12 +124,25 @@ class MemoryStats end def start - apache_processes = list_processes(:exe => PlatformInfo::HTTPD) + if PlatformInfo.httpd.nil? + STDERR.puts "*** ERROR: The Apache executable cannot be found." + STDERR.puts "Please set the APXS2 environment variable to your 'apxs2' " << + "executable's filename, or set the HTTPD environment variable " << + "to your 'httpd' or 'apache2' executable's filename." + exit 1 + end + + apache_processes = list_processes(:exe => PlatformInfo.httpd) + if apache_processes.empty? + # On some Linux distros, the Apache worker processes + # are called "httpd.worker" + apache_processes = list_processes(:exe => "#{PlatformInfo.httpd}.worker") + end print_process_list("Apache processes", apache_processes) puts passenger_processes = list_processes(:match => - /(^Passenger |^Rails:|^Rack:|ApplicationPoolServerExecutable)/) + /((^| )Passenger |(^| )Rails:|(^| )Rack:|ApplicationPoolServerExecutable)/) print_process_list("Passenger processes", passenger_processes, :show_ppid => false) if RUBY_PLATFORM !~ /linux/ @@ -141,9 +169,19 @@ class MemoryStats def list_processes(options) if options[:exe] name = options[:exe].sub(/.*\/(.*)/, '\1') - ps = "ps -C '#{name}'" + if RUBY_PLATFORM =~ /linux/ + ps = "ps -C '#{name}'" + else + ps = "ps -A" + options[:match] = Regexp.new(Regexp.escape(name)) + end elsif options[:name] - ps = "ps -C '#{options[:name]}'" + if RUBY_PLATFORM =~ /linux/ + ps = "ps -C '#{options[:name]}'" + else + ps = "ps -A" + options[:match] = Regexp.new(" #{Regexp.escape(options[:name])}") + end elsif options[:match] ps = "ps -A" else @@ -151,21 +189,37 @@ class MemoryStats end processes = [] - list = `#{ps} -w -o pid,ppid,nlwp,vsz,rss,command`.split("\n") + case RUBY_PLATFORM + when /solaris/ + list = `#{ps} -o pid,ppid,nlwp,vsz,rss,comm`.split("\n") + threads_known = true + when /darwin/ + list = `#{ps} -w -o pid,ppid,vsz,rss,command`.split("\n") + threads_known = false + else + list = `#{ps} -w -o pid,ppid,nlwp,vsz,rss,command`.split("\n") + threads_known = true + end list.shift list.each do |line| line.gsub!(/^ */, '') line.gsub!(/ *$/, '') p = Process.new - p.pid, p.ppid, p.threads, p.vm_size, p.rss, p.name = line.split(/ +/, 6) + if threads_known + p.pid, p.ppid, p.threads, p.vm_size, p.rss, p.name = line.split(/ +/, 6) + else + p.pid, p.ppid, p.vm_size, p.rss, p.name = line.split(/ +/, 6) + p.threads = "?" + end p.name.sub!(/\Aruby: /, '') p.name.sub!(/ \(ruby\)\Z/, '') if p.name !~ /^ps/ && (!options[:match] || p.name.match(options[:match])) # Convert some values to integer. - [:pid, :ppid, :threads, :vm_size, :rss].each do |attr| + [:pid, :ppid, :vm_size, :rss].each do |attr| p.send("#{attr}=", p.send(attr).to_i) end + p.threads = p.threads.to_i if threads_known if platform_provides_private_dirty_rss_information? p.private_dirty_rss = determine_private_dirty_rss(p.pid) diff --git a/bin/passenger-spawn-server b/bin/passenger-spawn-server index 8a267eeb..2e03ebef 100755 --- a/bin/passenger-spawn-server +++ b/bin/passenger-spawn-server @@ -41,13 +41,13 @@ begin exit end - require 'passenger/spawn_manager' - spawn_manager = Passenger::SpawnManager.new + require 'phusion_passenger/spawn_manager' + spawn_manager = PhusionPassenger::SpawnManager.new spawn_manager.start_synchronously(input) spawn_manager.cleanup rescue => e - require 'passenger/utils' - include Passenger::Utils + require 'phusion_passenger/utils' + include PhusionPassenger::Utils print_exception("spawn manager", e) exit 10 end diff --git a/bin/passenger-status b/bin/passenger-status index f91595b4..9a22a857 100755 --- a/bin/passenger-status +++ b/bin/passenger-status @@ -19,63 +19,38 @@ $LOAD_PATH << File.expand_path(File.dirname(__FILE__) + "/../lib") $LOAD_PATH << File.expand_path(File.dirname(__FILE__) + "/../ext") +require 'phusion_passenger/admin_tools/control_process' -class StatusFifo - attr_accessor :filename - attr_accessor :pid - - def initialize(filename, pid) - @filename = filename - @pid = pid - end -end +include PhusionPassenger::AdminTools -# Returns an array of all status FIFO files that are alive, -# and attempt to remove stale status FIFO files. -def list_and_clean_status_fifos - result = [] - Dir["/tmp/passenger_status.*.fifo"].each do |filename| - filename =~ /(\d+).fifo$/ - pid = $1.to_i - if process_is_alive?(pid) - result << StatusFifo.new(filename, pid) - else - puts "*** NOTICE: Removing stale status FIFO file #{filename}" - File.unlink(filename) rescue nil - end - end - return result -end +# ANSI color codes +RESET = "\e[0m" +BOLD = "\e[1m" +YELLOW = "\e[33m" +BLUE_BG = "\e[44m" -def process_is_alive?(pid) +def show_status(control_process, format = :text) begin - Process.kill(0, pid) - return true - rescue Errno::ESRCH - return false + text = control_process.status rescue SystemCallError => e - return true - end -end - -def show_status(status_fifo) - begin - puts File.read(status_fifo.filename) - rescue => e - STDERR.puts "*** ERROR: Cannot query status for Passenger instance #{status_fifo.pid}:" + STDERR.puts "*** ERROR: Cannot query status for Passenger instance #{control_process.pid}:" STDERR.puts e.to_s exit 2 end + # Colorize output + text.gsub!(/^(----)(.*)$/, YELLOW + BLUE_BG + BOLD + '\1\2' + RESET) + text.gsub!(/^( +in '.*? )(.*?)\(/, '\1' + BOLD + '\2' + RESET + '(') + puts text end def start if ARGV.empty? - status_fifos = list_and_clean_status_fifos - if status_fifos.size == 0 + control_processes = ControlProcess.list + if control_processes.empty? STDERR.puts("ERROR: Phusion Passenger doesn't seem to be running.") exit 2 - elsif status_fifos.size == 1 - show_status(status_fifos[0]) + elsif control_processes.size == 1 + show_status(control_processes.first) else puts "It appears that multiple Passenger instances are running. Please select a" puts "specific one by running:" @@ -83,13 +58,13 @@ def start puts " passenger-status " puts puts "The following Passenger instances are running:" - status_fifos.each do |status_fifo| - puts " PID: #{status_fifo.pid}" + control_processes.each do |control| + puts " PID: #{control.pid}" end exit 1 end else - show_status(StatusFifo.new("/tmp/passenger_status.#{ARGV[0]}.fifo", ARGV[0].to_i)) + show_status(ControlProcess.new(ARGV[0].to_i)) end end diff --git a/bin/passenger-stress-test b/bin/passenger-stress-test index 3a20c527..b033a92a 100755 --- a/bin/passenger-stress-test +++ b/bin/passenger-stress-test @@ -23,12 +23,12 @@ require 'rubygems' require 'optparse' require 'socket' require 'thread' -require 'passenger/platform_info' -require 'passenger/message_channel' -require 'passenger/utils' +require 'phusion_passenger/platform_info' +require 'phusion_passenger/message_channel' +require 'phusion_passenger/utils' -include Passenger -include Passenger::Utils +include PhusionPassenger +include PhusionPassenger::Utils include PlatformInfo # A thread or a process, depending on the Ruby VM implementation. diff --git a/debian/compat b/debian/compat deleted file mode 100644 index 7ed6ff82..00000000 --- a/debian/compat +++ /dev/null @@ -1 +0,0 @@ -5 diff --git a/debian/control b/debian/control deleted file mode 100644 index 5f415b11..00000000 --- a/debian/control +++ /dev/null @@ -1,21 +0,0 @@ -Package: passenger -Version: 1.1.0 -Section: net -Priority: optional -Architecture: any -Essential: no -Depends: -Pre-Depends: -XSB-Comment: We put dependencies in Recommends because a lot of people - have Ruby/RubyGems/Apache installed from source instead of from apt. -Recommends: ruby1.8, rubygems, apache2 -Suggests: -Maintainer: Hongli Lai -Conflicts: -Replaces: -Provides: -Installed-Size: 1741 -Description: Phusion Passenger, Ruby web application deployment module for Apache. - Phusion Passenger makes deploying Ruby and Ruby on Rails web applications on Apache - a breeze. Please don't forget to run 'passenger-install-apache2-module' after the - installation. diff --git a/debian/postinst b/debian/postinst deleted file mode 100755 index c9fe78dc..00000000 --- a/debian/postinst +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/sh -if ruby -e ''; then - if ! ruby -rpassenger/native_support -e '' 2>/dev/null; then - export RUBYLIB=/usr/lib/ruby/1.8 - fi - passenger-install-apache2-module - if ! ruby -rubygems -rfastthread -e '' 2>/dev/null; then - echo - echo - echo "****** WARNING WARNING WARNING WARNING WARNING WARNING WARNING ******" - echo - echo "Passenger requires the Ruby library 'fastthread', which does not seem to be installed. Since there's no native package for it in the Debian repository, you must install it via RubyGems:" - echo - echo " gem install fastthread" - echo - echo "Please install fastthread, otherwise Passenger won't work." - echo - echo "****** WARNING WARNING WARNING WARNING WARNING WARNING WARNING ******" - fi -else - echo "********* NOTE ***********" - echo "Ruby doesn't seem to be installed. Please install Ruby and RubyGems, because Passenger needs them." -fi -exit 0 diff --git a/debian/prerm b/debian/prerm deleted file mode 100755 index 13f47935..00000000 --- a/debian/prerm +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh - diff --git a/doc/ApplicationPool algorithm.txt b/doc/ApplicationPool algorithm.txt index 508c010e..1f5d61e8 100644 --- a/doc/ApplicationPool algorithm.txt +++ b/doc/ApplicationPool algorithm.txt @@ -3,17 +3,27 @@ == Introduction -For efficiency reasons, Passenger keeps a pool spawned Rails applications. +For efficiency reasons, Passenger keeps a pool spawned Rails/Ruby applications. Please read the C++ API documentation for the ApplicationPool class for a full -introduction. This document describes an algorithm for managing the pool. +introduction. This document describes an algorithm for managing the pool, in a +high-level way. The algorithm should strive to keep spawning to a minimum. -TODO: check whether the algorithm has thrashing behavior. - == Definitions +=== Vocabulary + +- "Application root": + The toplevel directory in which an application is contained. For Rails + application, this is the same as RAILS_ROOT, i.e. the directory that contains + "app/", "public/", etc. For a Rack application, this is the directory that + contains "config.ru". + +- "Active application instance": + An application instance that has more than 0 active sessions. + === Types Most of the types that we use in this document are pretty standard. But we @@ -35,13 +45,44 @@ explicitly define some special types: Moves the specified element to the front of the list. _iterator_ is an iterator, as described earlier. +- Domain + A compound type (class) which contains information about an application root, + such as the application instances that have been spawned for this application + root. + + A Domain has the following members: + * instances (list) - a list of AppContainer objects. + + Invariant: + containers is non-empty. + for all 0 <= i < containers.size() - 1: + if containers[i].app is active: + containers[i + 1].app is active + + * size (unsigned integer): The number of items in _instances_. + + * max_requests (unsigned integer): The maximum number of requests that each + application instance in this domain may process. After having processed this + many requests, the application instance will be shut down. + A value of 0 indicates that there is no maximum. + + * restart_file_checker + An object which monitors the restart.txt file, which belongs to this + application root, for changes. This object has the method changed(), + which rteurns whether restart.txt's timestamp has changed since the last + check. + - AppContainer - A compound type which contains an application instance, as well as iterators - for various linked lists. These iterators make it possible to perform actions - on the linked list in O(1) time. + A compound type (class) which contains an application instance, as well as + iterators for various linked lists. These iterators make it possible to + perform actions on the linked list in O(1) time. An AppContainer has the following members: * app - An Application object, representing an application instance. + * start_time (time) - The time at which this application instance was + started. It's set to the current time by AppContainer's constructor. + * processed_requests (integer) - The number of requests processed by this + application instance so far. * last_used (time) - The last time a session for this application instance was opened or closed. * sessions (integer) - The number of open sessions for this application @@ -49,11 +90,24 @@ explicitly define some special types: Invariant: (sessions == 0) == (This AppContainer is in inactive_apps.) * iterator - The iterator for this AppContainer in the linked list - apps[app.app_root] + domains[app.app_root].instances * ia_iterator - The iterator for this AppContainer in the linked list inactive_apps. This iterator is only valid if this AppContainer really is in that list. +- PoolOptions + A structure containing additional information used by the spawn manager's + spawning process, as well as by the get() function. + + A PoolOptions has the following members: + * max_requests (unsigned integer) - The maximum number of requests that the + application instance may process. After having processed this many requests, + the application instance will be shut down. A value of 0 indicates that there + is no maximum. + * use_global_queue (boolean) - Whether to use a global queue for all + application instances, or a queue that's private to the application instance. + The users guide explains this feature in more detail. + === Special functions - spawn(app_root) @@ -71,33 +125,29 @@ information: is non-recursive, i.e. if a thread locks a mutex that it has already locked, then it will result in a deadlock. -- apps: map[string => list] - Maps an application root to a list of AppContainers. Thus, this map contains - all application instances that are in the pool. +- domains: map[string => Domain] + Maps an application root to its Domain object. This map contains all + application instances in the pool. Invariant: - for all values v in app: - v is nonempty. - for all 0 <= i < v.size() - 1: - if v[i].app is active: - v[i + 1].app is active + for all values d in domains: + d.size <= count + (sum of all d.size in domains) == count - An active application is one that has more than 0 active sessions. - - max: integer - The maximum number of AppContainer objects that may exist in 'apps'. + The maximum number of AppContainer objects that may exist in the pool. - max_per_app: integer - The maximum number of concurrent AppContainer objects a single - application may spawn. + The maximum number of AppContainer objects that may be simultaneously alive + for a single Domain. - count: integer - The current number of AppContainer objects in 'apps'. + The current number of AppContainer objects in the pool. Since 'max' can be set dynamically during the life time of an application pool, 'count > max' is possible. - active: integer - The number of application instances in 'apps' that are active. + The number of application instances in the pool that are active. Invariant: active <= count @@ -108,31 +158,29 @@ information: Invariant: inactive_apps.size() == count - active for all c in inactive_apps: - c is in apps. + c can be found in _domains_. c.sessions == 0 -- restart_file_times: map[string => time] - Maps an application root to the last known modification time of - 'restart.txt'. - - Invariant: - for all keys app_root in restart_times: - apps.has_key(app_root) - - waiting_on_global_queue: integer If global queuing mode is enabled, then when get() is waiting for a backend process to become idle, this variable will be incremented. When get() is done waiting, this variable will be decremented. -- app_instance_count: map[string => unsigned int] - Maps an application root to the number of spawned applications. - Invariant: - app_instance_count.keys == apps.keys - for all (app_root, app_count) in app_instance_count: - app_instance_count[app_root] < count - app_count == apps[app_root].size() - (sum of all values in app_instance_count) == count. +== Class relations + +Here's an UML diagram in ASCII art: + +[AppContainer] 1..* -------+ + | + | + + 1 +[ApplicationPool] [Domain] + 1 0..* + + | | + +-------------------+ == Algorithm in pseudo code @@ -140,64 +188,68 @@ information: # Thread-safetiness notes: # - All wait commands are to unlock the lock during waiting. -function get(app_root): +# Connect to an existing application instance or to a newly spawned application instance. +# 'app_root' specifies the application root folder of the application. 'options' is an +# object of type 'PoolOptions', which contains additional information which may be +# relevant for spawning. +function get(app_root, options): MAX_ATTEMPTS = 10 attempt = 0 time_limit = now() + 5 seconds lock.synchronize: while (true): attempt++ - container, list = spawn_or_use_existing(app_root) + container, domain = spawn_or_use_existing(app_root, options) container.last_used = current_time() container.sessions++ try: return container.app.connect() on exception: + # The app instance seems to have crashed. + # So we remove this instance from our data + # structures. container.sessions-- + instances = domain.instances + instances.remove(container.iterator) + domain.size-- + if instances.empty(): + domains.remove(app_root) + count-- + active-- if (attempt == MAX_ATTEMPTS): propagate exception - else: - # The app instance seems to have crashed. - # So we remove this instance from our data - # structures. - list.remove(container.iterator) - if list.empty(): - apps.remove(app_root) - app_instance_count.remove(app_root) - count-- - active-- -# Returns a pair of [AppContainer, list] that matches the -# given application root. If no such AppContainer exists, then it is created -# and a new application instance is spawned. All exceptions that occur are -# propagated. -function spawn_or_use_existing(app_root): - list = apps[app_root] +# Returns a pair of [AppContainer, Domain] that matches the given application +# root. If no such AppContainer exists, then it is created and a new +# application instance is spawned. All exceptions that occur are propagated. +function spawn_or_use_existing(app_root, options): + domain = domains[app_root] - if (list != nil) and (needs_restart(app_root)): - for all container in list: + if (domain != nil) and (needs_restart(app_root, domain)): + for all container in domain.instances: if container.sessions == 0: inactive_apps.remove(container.ia_iterator) else: active-- - list.remove(container.iterator) + domain.instances.remove(container.iterator) count-- - apps.remove(app_root) - app_instance_count.remove(app_root) + domains.remove(app_root) list = nil Tell spawn server to reload code for app_root. - if list != nil: + if domain != nil: # There are apps for this app root. - if (list.front.sessions == 0): + instances = domain.instances + + if (instances.front.sessions == 0): # There is an inactive app, so we use it. - container = list.front - list.move_to_back(container.iterator) + container = instances.front + instances.move_to_back(container.iterator) inactive_apps.remove(container.ia_iterator) active++ else if (count >= max) or ( - (max_per_app != 0) and (app_instance_count[app_root] >= max_per_app) + (max_per_app != 0) and (domain.size >= max_per_app) ): # All apps are active, and the pool is full. # -OR- @@ -205,7 +257,7 @@ function spawn_or_use_existing(app_root): # spawned for this application domain has been reached. # # We're not allowed to spawn a new application instance. - if use_global_queue: + if options.use_global_queue: # So we wait until _active_ has changed, then # we restart this function and try again. waiting_on_global_queue++ @@ -216,8 +268,8 @@ function spawn_or_use_existing(app_root): # So we connect to an already active application. # This connection will be put into that # application's private queue. - container = a container in _list_ with the smallest _session_ value - list.move_to_back(container.iterator) + container = a container in _instances_ with the smallest _session_ value + instances.move_to_back(container.iterator) else: # All apps are active, but the pool hasn't reached its # maximum yet. So we spawn a new app. @@ -225,9 +277,9 @@ function spawn_or_use_existing(app_root): # TODO: we should add some kind of timeout check for spawning. container.app = spawn(app_root) container.sessions = 0 - iterator = list.add_to_back(container) + iterator = instances.add_to_back(container) container.iterator = iterator - app_instance_count[app_root]++ + domain.size++ count++ active++ else: @@ -239,11 +291,7 @@ function spawn_or_use_existing(app_root): if (active >= max): wait until _active_ has changed goto beginning of function - # FIXME: there's a possible concurrency issue here. After - # waiting and having reacquired the lock, the state might have - # been completely changed, and there may now exist an instance - # of this application domain. - if count == max: + elsif count == max: # Here we are in a though situation. There are several # apps which are inactive, and none of them have # application root _app_root_, so we must kill one of @@ -256,31 +304,33 @@ function spawn_or_use_existing(app_root): # killed. But for now, we kill a random application # instance. container = inactive_apps.pop_front - list = apps[container.app.app_root] - list.remove(container.iterator) - if list.empty(): - apps.remove(container.app.app_root) - restart_file_times.remove(container.app.app_root) - app_instance_count.remove(container.app.app_root) + domain = domains[container.app.app_root] + instances = domain.instances + instances.remove(container.iterator) + if instances.empty(): + domains.remove(container.app.app_root) else: - app_instance_count[container.app.app_root]-- + domain.size-- count-- container = new AppContainer # TODO: we should add some kind of timeout check for spawning. container.app = spawn(app_root) container.sessions = 0 - list = apps[app_root] - if list == nil: - list = new list - apps[app_root] = list - app_instance_count[app_root] = 1 + domain = domains[app_root] + if domain == nil: + domain = new Domain + initialize domain.instances + initialize domain.restart_file_checker with "$app_root/tmp/restart.txt" + domain.size = 1 + domain.max_requests = options.max_requests + domains[app_root] = domain else: - app_instance_count[app_root]++ - iterator = list.add_to_back(container) + domain.size++ + iterator = domain.instances.add_to_back(container) container.iterator = iterator count++ active++ - return [container, list] + return [container, domain] # The following function is to be called when a session has been closed. @@ -288,39 +338,39 @@ function spawn_or_use_existing(app_root): # session has been closed. function session_has_been_closed(container): lock.synchronize: - list = apps[container.app.app_root] - if list != nil: - container.last_used = current_time() - container.sessions-- - if container.sessions == 0: - list.move_to_front(container.iterator) - container.ia_iterator = inactive_apps.add_to_back(container.app) + domain = domains[container.app.app_root] + if domain != nil: + instances = domain.instances + container.processed++ + + if (domain.max_requests) > 0 and (container.processed >= domain.max_requests): + # The application instance has processed its maximum allowed + # number of requests, so we shut it down. + instances.remove(container.iterator) + domain.size-- + if instances.empty(): + domains.remove(app_root) + count-- active-- + else: + container.last_used = current_time() + container.sessions-- + container.processed++ + if container.sessions == 0: + instances.move_to_front(container.iterator) + container.ia_iterator = inactive_apps.add_to_back(container.app) + active-- -function needs_restart(app_root): - restart_file = "$app_root/tmp/restart.txt" - s = stat(restart_file) - if s != null: - delete_file(restart_file) - if (deletion was successful) or (file was already deleted): - restart_file_times.remove(app_root) - result = true - else: - last_restart_file_time = restart_file_times[app_root] - if last_restart_time == null: - result = true - else: - result = s.mtime != last_restart_file_time - restart_file_times[app_root] = s.mtime - else: - restart_file_times.remove(app_root) - result = false - return result +function needs_restart(app_root, domain): + always_restart_file = "$app_root/tmp/always_restart.txt" + return (file_exists(always_restart_file)) or + (domain.restart_file_checker.changed()) # The following thread will be responsible for cleaning up idle application # instances, i.e. instances that haven't been used for a while. +# This can be disabled per app when setting it's maxIdleTime to 0. thread cleaner: lock.synchronize: done = false @@ -333,14 +383,16 @@ thread cleaner: now = current_time() for all container in inactive_apps: app = container.app - app_list = apps[app.app_root] - if now - container.last_used > MAX_IDLE_TIME: - app_list.remove(container.iterator) - inactive_apps.remove(iterator for container) - app_instance_count[app.app_root]-- + domain = domains[app.app_root] + instances = domain.instances + # If MAX_IDLE_TIME is 0 we don't clean up the instance, + # giving us the option to persist the app container + # forever unless it's killed by another app. + if (MAX_IDLE_TIME > 0) and (now - container.last_used > MAX_IDLE_TIME): + instances.remove(container.iterator) + inactive_apps.remove(container.iterator) + domain.size-- count-- - if app_list.empty(): - apps.remove(app.app_root) - app_instance_count.remove(app.app_root) - restart_file_times.remove(app.app_root) + if instances.empty(): + domains.remove(app.app_root) diff --git a/doc/Users guide.txt b/doc/Users guide.txt index aa9331bf..ee279dd6 100644 --- a/doc/Users guide.txt +++ b/doc/Users guide.txt @@ -28,6 +28,7 @@ Phusion Passenger has been tested on: - Ubuntu Linux 6.06 (x86) - Ubuntu Linux 7.10 (x86) +- Ubuntu Linux 8.04 (x86) - Debian Sarge (x86) - Debian Etch (x86) - Debian Lenny/Sid (x86) @@ -288,7 +289,7 @@ link:http://rack.rubyforge.org/[Rack] interface. Phusion Passenger assumes that Rack application directories have a certain layout. Suppose that you have a Rack application in '/webapps/rackapp'. Then that -folder must contain at least two entries: +folder must contain at least three entries: - 'config.ru', a Rackup file for starting the Rack application. This file must contain the complete logic for initializing the application. @@ -472,8 +473,10 @@ run Mack::Utils::Server.build_app require 'rubygems' require 'merb-core' -Merb::Config.setup(:merb_root => ".", - :environment => ENV['RACK_ENV']) +Merb::Config.setup( + :merb_root => File.expand_path(File.dirname(__FILE__)), + :environment => ENV['RACK_ENV'] +) Merb.environment = Merb::Config[:environment] Merb.root = Merb::Config[:merb_root] Merb::BootLoader.run @@ -496,14 +499,12 @@ require 'sinatra' root_dir = File.dirname(__FILE__) -Sinatra::Application.default_options.merge!( - :views => File.join(root_dir, 'views'), - :app_file => File.join(root_dir, 'app.rb'), - :run => false, - :env => ENV['RACK_ENV'].to_sym -) +set :environment, ENV['RACK_ENV'].to_sym +set :root, root_dir +set :app_file, File.join(root_dir, 'app.rb') +disable :run -run Sinatra.application +run Sinatra::Application ------------------------------------------------------ @@ -545,12 +546,43 @@ This option allows one to specify the Ruby interpreter to use. This option may only occur once, in the global server configuration. The default is 'ruby'. +[[PassengerAppRoot]] +=== PassengerAppRoot === +By default, Phusion Passenger assumes that the application's root directory +is the parent directory of the 'public' directory. This option allows one to +specify the application's root independently from the DocumentRoot, which +is useful if the 'public' directory lives in a non-standard place. + +This option may occur in the following places: + + * In the global server configuration. + * In a virtual host configuration block. + * In a `` or `` block. + * In '.htaccess', if `AllowOverride Options` is on. + +In each place, it may be specified at most once. + +Example: + +----------------------------- + + DocumentRoot /var/rails/zena/sites/example.com/public + PassengerAppRoot /var/rails/zena # <-- normally Phusion Passenger would + # have assumed that the application + # root is "/var/rails/zena/sites/example.com" + +----------------------------- + [[PassengerUseGlobalQueue]] === PassengerUseGlobalQueue === Turns the use of global queuing on or off. -This option may only occur once, in the global server configuration. The -default is 'off'. +This option may occur in the following places: + + * In the global server configuration. + * In a virtual host configuration block. + +In each place, it may be specified at most once. The default value is 'off'. 'This feature is sponsored by http://www.37signals.com/[37signals].' @@ -650,6 +682,197 @@ applications must run as, if user switching fails or is disabled. This option may only occur once, in the global server configuration. The default value is 'nobody'. +[[PassengerHighPerformance]] +=== PassengerHighPerformance === +By default, Phusion Passenger is compatible with mod_rewrite and most other +Apache modules. However, a lot of effort is required in order to be compatible. +If you turn 'PassengerHighPerformance' to 'on', then Phusion Passenger will be +a little faster, in return for reduced compatibility with other Apache modules. + +In places where 'PassengerHighPerformance' is turned on, mod_rewrite rules will +likely not work. mod_autoindex (the module which displays a directory index) +will also not work. Other Apache modules may or may not work, depending on what +they exactly do. We recommend you to find out how other modules behave in high +performance mode via testing. + +This option is *not* an all-or-nothing global option: you can enable high +performance mode for certain virtual hosts or certain URLs only. +The 'PassengerHighPerformance' option may occur in the following places: + + * In the global server configuration. + * In a virtual host configuration block. + * In a `` or `` block. + * In '.htaccess'. + +In each place, it may be specified at most once. The default value is 'off', +so high performance mode is disabled by default, and you have to explicitly +enable it. + +.When to enable high performance mode? + +If you do not use mod_rewrite or other Apache modules then it might make +sense to enable high performance mode. + +It's likely that some of your applications depend on mod_rewrite or other +Apache modules, while some do not. In that case you can enable high performance +for only those applications that don't use other Apache modules. For example: + +------------------------------------ + + ServerName www.foo.com + DocumentRoot /apps/foo/public + .... mod_rewrite rules or options for other Apache modules here ... + + + + ServerName www.bar.com + DocumentRoot /apps/bar/public + PassengerHighPerformance on + +------------------------------------ + +In the above example, high performance mode is only enabled for www.bar.com. +It is disabled for everything else. + +If your application generally depends on mod_rewrite or other Apache modules, +but a certain URL that's accessed often doesn't depend on those other modules, +then you can enable high performance mode for a certain URL only. For example: + +------------------------------------ + + ServerName www.foo.com + DocumentRoot /apps/foo/public + .... mod_rewrite rules or options for other Apache modules here ... + + + PassengerHighPerformance on + + +------------------------------------ + +This enables high performance mode for +http://www.foo.com/chatroom/ajax_update_poll only. + +=== PassengerEnabled === +You can set this option to 'off' to completely disable Phusion Passenger for +a certain location. This is useful if, for example, you want to integrate a PHP +application into the same virtual host as a Rails application. + +Suppose that you have a Rails application in '/apps/foo'. Suppose that you've +dropped Wordpress -- a blogging application written in PHP -- in +'/apps/foo/public/wordpress'. You can then configure Phusion Passenger as +follows: + +------------------------------------ + + ServerName www.foo.com + DocumentRoot /apps/foo/public + + PassengerEnabled off + AllowOverride all # <-- Makes Wordpress's .htaccess file work. + + +------------------------------------ + +This way, Phusion Passenger will not interfere with Wordpress. + +'PassengerEnabled' may occur in the following places: + + * In the global server configuration. + * In a virtual host configuration block. + * In a `` or `` block. + * In '.htaccess'. + +In each place, it may be specified at most once. The default value is 'on'. + +=== PassengerTempDir === +Specifies the directory that Phusion Passenger should use for storing temporary +files. This includes things such as Unix socket files, buffered file uploads, +etc. + +This option may be specified once, in the global server configuration. The +default temp directory that Phusion Passenger uses is '/tmp'. + +This option is especially useful if Apache is not allowed to write to /tmp +(which is the case on some systems with strict SELinux policies) or if the +partition that /tmp lives on doesn't have enough disk space. + +.Command line tools +Some Phusion Passenger command line administration tools, such as +`passenger-status`, must know what Phusion Passenger's temp directory is +in order to function properly. You can pass the directory through the +`PASSENGER_TMPDIR` environment variable, or the `TMPDIR` environment variable +(the former will be used if both are specified). + +For example, if you set 'PassengerTempDir' to '/my_temp_dir', then invoke +`passenger-status` after you've set the `PASSENGER_TMPDIR` or `TMPDIR` +environment variable, like this: + +---------------------------------------------------------- +export PASSENGER_TMPDIR=/my_temp-dir +sudo -E passenger-status +# The -E option tells 'sudo' to preserve environment variables. +---------------------------------------------------------- + +=== PassengerRestartDir === +As described in the deployment chapters of this document, Phusion Passenger +checks the file 'tmp/restart.txt' in the applications' +<> for restarting applications. Sometimes it +may be desirable for Phusion Passenger to look in a different directory instead, +for example for security reasons (see below). This option allows you to +customize the directory in which 'restart.txt' is searched for. + +You may specify 'PassengerRestartDir' in the following places: + + * In the global server configuration. + * In a virtual host configuration block. + * In a `` or `` block. + * In '.htaccess', if `AllowOverrides Options` is enabled. + +In each place, it may be specified at most once. + +You can either set it to an absolute directory, or to a directory relative to +the <>. Examples: + +----------------------------------- + + ServerName www.foo.com + # Phusion Passenger will check for /apps/foo/public/tmp/restart.txt + DocumentRoot /apps/foo/public + + + + ServerName www.bar.com + DocumentRoot /apps/bar/public + # An absolute filename is given; Phusion Passenger will + # check for /restart_files/bar/restart.txt + PassengerRestartDir /restart_files/bar + + + + ServerName www.baz.com + DocumentRoot /apps/baz/public + # A relative filename is given; Phusion Passenger will + # check for /apps/baz/restart_files/restart.txt + # + # Note that this directory is relative to the APPLICATION ROOT, *not* + # the value of DocumentRoot! + PassengerRestartDir restart_files + +----------------------------------- + +.What are the security reasons for wanting to customize PassengerRestartDir? +Touching restart.txt will cause Phusion Passenger to restart the application. +So anybody who can touch restart.txt can effectively cause a Denial-of-Service +attack by touching restart.txt over and over. If your web server or one of your +web applications has the permission to touch restart.txt, and one of them has a +security flaw which allows an attacker to touch restart.txt, then that will +allow the attacker to cause a Denial-of-Service. + +You can prevent this from happening by pointing PassengerRestartDir to a +directory that's readable by Apache, but only writable by administrators. + + === Resource control and optimization options === ==== PassengerMaxPoolSize ==== @@ -705,9 +928,75 @@ Rails/Rack web page. We recommend a value of `2 * x`, where `x` is the average number of seconds that a visitor spends on a single Rails/Rack web page. But your mileage may vary. +When this value is set to '0', application instances will not be shutdown unless +it's really necessary, i.e. when Phusion Passenger is out of worker processes +for a given application and one of the inactive application instances needs to +make place for another application instance. Setting the value to 0 is +recommended if you're on a non-shared host that's only running a few +applications, each which must be available at all times. + This option may only occur once, in the global server configuration. The default value is '300'. +[[PassengerMaxRequests]] +==== PassengerMaxRequests ==== +The maximum number of requests an application instance will process. After +serving that many requests, the application instance will be shut down and +Phusion Passenger will restart it. A value of 0 means that there is no maximum: +an application instance will thus be shut down when its idle timeout has been +reached. + +This option is useful if your application is leaking memory. By shutting +it down after a certain number of requests, all of its memory is guaranteed +to be freed by the operating system. + +This option may occur in the following places: + + * In the global server configuration. + * In a virtual host configuration block. + * In a `` or `` block. + * In '.htaccess', if `AllowOverride Limits` is on. + +In each place, it may be specified at most once. The default value is '0'. + +[CAUTION] +===================================================== +The <> directive should be considered +as a workaround for misbehaving applications. It is advised that you fix the +problem in your application rather than relying on these directives as a +measure to avoid memory leaks. +===================================================== + +==== PassengerStatThrottleRate ==== +By default, Phusion Passenger performs several filesystem checks (or, in +programmers jargon, 'stat() calls') each time a request is processed: + +- It checks whether 'config/environment.rb', 'config.ru' or 'passenger_wsgi.py' + is present, in order to autodetect Rails, Rack and WSGI applications. +- It checks whether 'restart.txt' has changed or whether 'always_restart.txt' + exists, in order to determine whether the application should be restarted. + +On some systems where disk I/O is expensive, e.g. systems where the harddisk is +already being heavily loaded, or systems where applications are stored on NFS +shares, these filesystem checks can incur a lot of overhead. + +You can decrease or almost entirely eliminate this overhead by setting +'PassengerStatThrottleRate'. Setting this option to a value of 'x' means that +the above list of filesystem checks will be performed at most once every 'x' +seconds. Setting it to a value of '0' means that no throttling will take place, +or in other words, that the above list of filesystem checks will be performed on +every request. + +This option may occur in the following places: + + * In the global server configuration. + * In a virtual host configuration block. + * In a `` or `` block. + * In '.htaccess', if `AllowOverride Limits` is on. + +In each place, it may be specified at most once. The default value is '0'. + + === Ruby on Rails-specific options === ==== RailsAutoDetect ==== @@ -751,23 +1040,25 @@ Used to specify that the given URI is a Rails application. See It is allowed to specify this option multiple times. Do this to deploy multiple Rails applications in different sub-URIs under the same virtual host. -This option may occur in the global server configuration or in a -virtual host configuration block. - -[[RailsAllowModRewrite]] -==== RailsAllowModRewrite ==== -If enabled, Phusion Passenger will not override mod_rewrite rules. Please read -<> for details. +This option may occur in the following places: -This option may occur once, in the global server configuration or in a virtual host -configuration block. The default value is 'off'. + * In the global server configuration. + * In a virtual host configuration block. + * In a `` or `` block. + * In '.htaccess', if `AllowOverride Options` is on. [[rails_env]] ==== RailsEnv ==== This option allows one to specify the default `RAILS_ENV` value. -This option may occur once, in the global server configuration or in a virtual host -configuration block. The default value is 'production'. +This option may occur in the following places: + + * In the global server configuration. + * In a virtual host configuration block. + * In a `` or `` block. + * In '.htaccess', if `AllowOverride Options` is on. + +In each place, it may be specified at most once. The default value is 'production'. [[RailsSpawnMethod]] ==== RailsSpawnMethod ==== @@ -779,11 +1070,11 @@ understand it, as it's mostly a technical detail. You can basically follow this ************************************************ If your application works on Mongrel, but not on Phusion Passenger, then set -`RailsSpawnMethod` to 'conservative'. Otherwise, leave it at 'smart' (the default). +`RailsSpawnMethod` to 'conservative'. Otherwise, leave it at 'smart-lv2' (the default). ************************************************ -However, we do recommend you to try to understand it. The 'smart' spawn method brings -many benefits. +However, we do recommend you to try to understand it. The 'smart' and 'smart-lv2' spawn +methods bring many benefits. ========================================================= Internally, Phusion Passenger spawns multiple Ruby on Rails processes in order to handle @@ -792,7 +1083,9 @@ its own set of pros and cons. Supported spawn methods are: 'smart':: When this spawn method is used, Phusion Passenger will attempt to cache Ruby on Rails -framework code and application code for a limited period of time. +framework code and application code for a limited period of time. Please read +<> for a more detailed +explanation of what smart spawning exactly does. + *Pros:* This can significantly decrease spawn time (by as much as 90%). And, when Ruby Enterprise @@ -803,9 +1096,26 @@ Some Ruby on Rails applications and libraries are not compatible with smart spaw If that's the case for your application, then you should use 'conservative' as spawning method. +'smart-lv2':: +This spawning method is similar to 'smart' but it skips the framework spawner +and uses the application spawner directly. This means the framework code is not +cached between multiple applications, although it is still cached within +instances of the same application. Please read +<> for a more detailed +explanation of what smart-lv2 spawning exactly does. ++ +*Pros:* It is compatible with a larger number of applications when compared to +the 'smart' method, and still performs some caching. ++ +*Cons:* It is slower than smart spawning if you have many applications which +use the same framework version. It is therefore advised that shared hosts use the +'smart' method instead. + 'conservative':: -This spawning method is similar to the one used in Mongrel Cluster. It does not perform -any code caching at all. +This spawning method is similar to the one used in Mongrel Cluster. It does not +perform any code caching at all. Please read +<> for a more detailed +explanation of what conservative spawning exactly does. + *Pros:* Conservative spawning is guaranteed to be compatible with all Rails applications @@ -816,8 +1126,52 @@ Much slower than smart spawning. Every spawn action will be equally slow, though the startup time of a single server in Mongrel Cluster. Conservative spawning will also render <> useless. -This option may occur once, in the global server configuration or in a virtual host -configuration block. The default value is 'smart'. +This option may occur in the following places: + + * In the global server configuration. + * In a virtual host configuration block. + +In each place, it may be specified at most once. The default value is 'smart-lv2'. + +==== RailsFrameworkSpawnerIdleTime ==== +The FrameworkSpawner server (explained in <>) has an idle timeout, just like the backend processes spawned by +Phusion Passenger do. That is, it will automatically shutdown if it hasn't done +anything for a given period. + +This option allows you to set the FrameworkSpawner server's idle timeout, in +seconds. A value of '0' means that it should never idle timeout. + +Setting a higher value will mean that the FrameworkSpawner server is kept around +longer, which may slightly increase memory usage. But as long as the +FrameworkSpawner server is running, the time to spawn a Ruby on Rails backend +process only takes about 40% of the time that is normally needed, assuming that +you're using the 'smart' <>. So if your +system has enough memory, is it recommended that you set this option to a high +value or to '0'. + +This option may only occur in the global server configuration, and may occur at +most once. The default value is '1800' (30 minutes). + +==== RailsAppSpawnerIdleTime ==== +The ApplicationSpawner server (explained in <>) has an idle timeout, just like the backend processes spawned by +Phusion Passenger do. That is, it will automatically shutdown if it hasn't done +anything for a given period. + +This option allows you to set the ApplicationSpawner server's idle timeout, in +seconds. A value of '0' means that it should never idle timeout. + +Setting a higher value will mean that the ApplicationSpawner server is kept around +longer, which may slightly increase memory usage. But as long as the +ApplicationSpawner server is running, the time to spawn a Ruby on Rails backend +process only takes about 10% of the time that is normally needed, assuming that +you're using the 'smart' or 'smart-lv2' <>. So if your +system has enough memory, is it recommended that you set this option to a high +value or to '0'. + +This option may only occur in the global server configuration, and may occur at +most once. The default value is '600' (10 minutes). === Rack-specific options === @@ -862,8 +1216,12 @@ Used to specify that the given URI is a Rack application. See It is allowed to specify this option multiple times. Do this to deploy multiple Rack applications in different sub-URIs under the same virtual host. -This option may occur in the global server configuration or in a -virtual host configuration block. +This option may occur in the following places: + + * In the global server configuration. + * In a virtual host configuration block. + * In a `` or `` block. + * In '.htaccess', if `AllowOverride Options` is on. [[rack_env]] ==== RackEnv ==== @@ -871,8 +1229,14 @@ The given value will be accessible in Rack applications in the `RACK_ENV` environment variable. This allows one to define the environment in which Rack applications are run, very similar to `RAILS_ENV`. -This option may occur once, in the global server configuration or in a virtual host -configuration block. The default value is 'production'. +This option may occur in the following places: + + * In the global server configuration. + * In a virtual host configuration block. + * In a `` or `` block. + * In '.htaccess', if `AllowOverride Options` is on. + +In each place, it may be specified at most once. The default value is 'production'. === Deprecated options === @@ -888,6 +1252,9 @@ Deprecated in favor of <>. ==== RailsDefaultUser ==== Deprecated in favor of <>. +==== RailsAllowModRewrite ==== +This option doesn't do anything anymore in recent versions of Phusion Passenger. + == Troubleshooting == @@ -1197,34 +1564,6 @@ chcon -R -h -t httpd_sys_content_t /path/to/your/rails/app [[conflicting_apache_modules]] === Conflicting Apache modules === -==== mod_rewrite and mod_alias ==== - -Phusion Passenger conflicts with 'mod_rewrite' and 'mod_alias'. Those modules may be -installed and loaded together with 'mod_passenger', and they will work fine -outside virtual hosts that contain a Rails application, but we recommend you -not to use their features inside virtual hosts that contain a Rails -application. - -By default, Phusion Passenger will override mod_rewrite rules on Rails hosts. -This is because the default .htaccess, as provided by Ruby on Rails, redirects all -requests to `dispatch.cgi' using mod_rewrite. This is a CGI application which -loads the entire Ruby on Rails framework for every request, and thus is very -slow. If we do not override mod_rewrite, then Ruby on Rails apps will be slow -on Phusion Passenger by default -- but we want a good out-of-the-box experience. - -Furthermore, the primary reason why people use mod_rewrite with Rails -applications, is to accelerate page caching. Phusion Passenger supports page -caching out-of-the-box, without mod_rewrite. - -It is not fully understood how mod_alias conflicts with Phusion Passenger, but we -recommend you not to use it on Rails virtual hosts. mod_alias rules can result -in surprising problems. - -If you really want to use mod_rewrite on Rails virtual hosts, then please set -the <> configuration option. But -please note that you will have to delete Rails applications' default .htaccess -file, or add rewrite rules to negate its effects. - ==== mod_userdir ==== 'mod_userdir' is not compatible with Phusion Passenger at the moment. @@ -1302,9 +1641,9 @@ count = 1 active = 0 inactive = 1 ------------ Applications ----------- +----------- Domains ----------- /var/www/projects/app1-foobar: - PID: 9617 Sessions: 0 + PID: 9617 Sessions: 0 Processed: 7 Uptime: 2m 23s -------------------------------------------------- The 'general information' section shows the following information: @@ -1317,30 +1656,39 @@ active:: The number of application instances that are currently processing requests. This value is always less than or equal to 'count'. inactive:: The number of application instances that are currently *not* processing requests, i.e. are idle. Idle application instances will be shutdown after a while, -as can be specified with <>. The value of 'inactive' -equals `count - active`. +as can be specified with <> (unless this +value is set to 0, in which case application instances are never shut down via idle +time). The value of 'inactive' equals `count - active`. + +The 'domains' section shows, for each application directory, information about running +application instances: -The 'applications' section shows each application instance, which directory it belongs -to. The 'sessions' field shows how many HTTP client are currently being processed by -that application instance. +Sessions:: Shows how many HTTP client are currently in the queue of that application +Instance, waiting to be processed. +Processed:: Indicates how many requests the instance has served until now. *Tip:* it's +possible to limit this number with the <> +configuration directive. +Uptime:: Shows for how long the application instance has been running. Since Phusion Passenger uses fair load balancing by default, the number of sessions for the application instances should be fairly close to each other. For example, this is fairly normal: -------------------------------- - PID: 4281 Sessions: 2 - PID: 4268 Sessions: 0 - PID: 4265 Sessions: 1 - PID: 4275 Sessions: 1 + PID: 4281 Sessions: 2 Processed: 7 Uptime: 5m 11s + PID: 4268 Sessions: 0 Processed: 5 Uptime: 4m 52s + PID: 4265 Sessions: 1 Processed: 6 Uptime: 5m 38s + PID: 4275 Sessions: 1 Processed: 7 Uptime: 3m 14s -------------------------------- But if you see a "spike", i.e. an application instance has an unusually high number of sessions compared to the others, then there might be a problem: -------------------------------- - PID: 4281 Sessions: 2 - PID: 17468 Sessions: 8 <---- "spike" - PID: 4265 Sessions: 1 - PID: 4275 Sessions: 1 + PID: 4281 Sessions: 2 Processed: 7 Uptime: 5m 11s + PID: 17468 Sessions: 8 <-+ Processed: 2 Uptime: 4m 47s + PID: 4265 Sessions: 1 | Processed: 6 Uptime: 5m 38s + PID: 4275 Sessions: 1 | Processed: 7 Uptime: 3m 14s + | + +---- "spike" -------------------------------- Possible reasons why spikes can occur: @@ -1376,7 +1724,7 @@ will restart killed application instances, as if nothing bad happened. [[user_switching]] === User switching (security) === -There is a problem that plagues most PHP web host, namely the fact that all PHP +There is a problem that plagues most PHP web hosts, namely the fact that all PHP applications are run in the same user context as the web server. So for example, Joe's PHP application will be able to read Jane's PHP application's passwords. This is obviously undesirable on many servers. @@ -1434,30 +1782,37 @@ role :web, domain role :db, domain, :primary => true namespace :deploy do + task :start, :roles => :app do + run "touch #{current_release}/tmp/restart.txt" + end + + task :stop, :roles => :app do + # Do nothing. + end + desc "Restart Application" task :restart, :roles => :app do - run "touch #{current_path}/tmp/restart.txt" + run "touch #{current_release}/tmp/restart.txt" end end -------------------------------------------------- -[NOTE] -========================================================================== -You may notice that for each deploy, a new spawner server is +You may notice that after each deploy, a new spawner server is created (it'll show up in `passenger-memory-stats`). Indeed, Capistrano will deploy -to a path ending with '/current' (ie : '/var/www/yourapp/current'), so that you don't +to a path ending with '/current' (ie : '/u/apps/yourapp/current'), so that you don't have to care about revisions in your virtual host configuration. This '/current' directory is a symlink to the current revision deployed ('/path_to_app/releases/date_of_the_release'). -Therefore, when deploying a new version, the symlink will change, and Phusion Passenger -will think it's a new application, thereby creating a new spawner server: +Phusion Passenger recognizes applications by their full canonical path, so after +deploying a new version, Phusion Passenger will think that the new version is +a totally different application, thereby creating a new spawner server: -------------------------------------------------- -1001 30291 [...] Passenger ApplicationSpawner: /var/www/my_app/releases/20080509104413 -1001 31371 [...] Passenger ApplicationSpawner: /var/www/my_app/releases/20080509104632 +1001 30291 [...] Passenger ApplicationSpawner: /u/apps/my_app/releases/20080509104413 +1001 31371 [...] Passenger ApplicationSpawner: /u/apps/my_app/releases/20080509104632 -------------------------------------------------- -Don't worry about this. The (old) spawner server will terminate itself after its default -timeout (10 minutes), so you will not run out of memory. +Don't worry about this. The (old) spawner server will terminate itself after its +timeout period (10 minutes by default), so you will not run out of memory. If you really want to release the spawner server's memory immediately, then you can add a command to your Capistrano script to terminate the Passenger spawn server after each deploy. That @@ -1469,7 +1824,7 @@ kill $( passenger-memory-stats | grep 'Passenger spawn server' | awk '{ print $1 Killing the spawn server is completely safe, because Phusion Passenger will restart the spawn server if it has been terminated. -========================================================================== + === Moving Phusion Passenger to a different directory === @@ -1517,6 +1872,24 @@ Phusion Passenger does not provide upload progress support by itself. Please try drogus's link:http://github.com/drogus/apache-upload-progress-module/tree/master[ Apache upload progress module] instead. +=== Making the application restart after each request === + +In some situations it might be desirable to restart the web application after +each request, for example when developing a non-Rails application that doesn't +support code reloading, or when developing a web framework. + +To achieve this, simply create the file 'tmp/always_restart.txt' in your +application's root folder. Unlike 'restart.txt', Phusion Passenger does not +delete this file. If both files are present ('restart.txt' and 'always_restart.txt'), +then Phusion Passenger will still restart the application, but won't delete +'restart.txt'. + +NOTE: If you're just developing a Rails application then you probably don't need +this feature. If you set 'RailsEnv development' in your Apache configuration, +then Rails will automatically reload your application code after each request. +'always_restart.txt' is only useful if you're working on Ruby on Rails itself, +or when you're not developing a Rails application and your web framework +does not support code reloading. == Appendix A: About this document == @@ -1532,3 +1905,372 @@ image:images/phusion_banner.png[link="http://www.phusion.nl/"] Phusion Passenger is a trademark of Hongli Lai & Ninh Bui. +== Appendix B: Terminology == + +[[application_root]] +=== Application root === +The root directory of an application that's served by Phusion Passenger. + +In case of Ruby on Rails applications, this is the directory that contains +'Rakefile', 'app/', 'config/', 'public/', etc. In other words, the directory +pointed to by `RAILS_ROOT`. For example, take the following directory structure: + +----------------------------------------- +/apps/foo/ <------ This is the Rails application's application root! + | + +- app/ + | | + | +- controllers/ + | | + | +- models/ + | | + | +- views/ + | + +- config/ + | | + | +- environment.rb + | | + | +- ... + | + +- public/ + | | + | +- ... + | + +- ... +----------------------------------------- + +In case of Rack applications, this is the directory that contains 'config.ru'. +For example, take the following directory structure: + +----------------------------------------- +/apps/bar/ <----- This is the Rack application's application root! + | + +- public/ + | | + | +- ... + | + +- config.ru + | + +- ... +----------------------------------------- + +In case of Python (WSGI) applications, this is the directory that contains +'passenger_wsgi.py'. For example, take the following directory structure: + +----------------------------------------- +/apps/baz/ <----- This is the WSGI application's application root! + | + +- public/ + | | + | +- ... + | + +- passenger_wsgi.py + | + +- ... +----------------------------------------- + + +[[spawning_methods_explained]] +== Appendix C: Spawning methods explained == + +At its core, Phusion Passenger is an HTTP proxy and process manager. It spawns +Ruby on Rails/Rack/WSGI worker processes (which may also be referred to as +'backend processes'), and forwards incoming HTTP request to one of the worker +processes. + +While this may sound simple, there's not just one way to spawn worker processes. +Let's go over the different spawning methods. For simplicity's sake, let's +assume that we're only talking about Ruby on Rails applications. + +=== The most straightforward and traditional way: conservative spawning === + +Phusion Passenger could create a new Ruby process, which will then load the +Rails application along with the entire Rails framework. This process will then +enter an request handling main loop. + +This is the most straightforward way to spawn worker processes. If you're +familiar with the Mongrel application server, then this approach is exactly +what mongrel_cluster performs: it creates N worker processes, each which loads +a full copy of the Rails application and the Rails framework in memory. The Thin +application server employs pretty much the same approach. + +Note that Phusion Passenger's version of conservative spawning differs slightly +from mongrel_cluster. Mongrel_cluster creates entirely new Ruby processes. In +programmers jargon, mongrel_cluster creates new Ruby processes by forking the +current process and exec()-ing a new Ruby interpreter. Phusion Passenger on the +other hand creates processes that reuse the already loaded Ruby interpreter. In +programmers jargon, Phusion Passenger calls fork(), but not exec(). + +=== The smart spawning method === + +NOTE: Smart spawning is only available for Ruby on Rails applications, not for + Rack applications or WSGI applications. + +While conservative spawning works well, it's not as efficient as it could be +because each worker process has its own private copy of the Rails application +as well as the Rails framework. This wastes memory as well as startup time. + +image:images/conservative_spawning.png[Worker processes and conservative spawning] + +'Figure: Worker processes and conservative spawning. Each worker process has its +own private copy of the application code and Rails framework code.' + +It is possible to make the different worker processes share the memory occupied +by application and Rails framework code, by utilizing so-called +copy-on-write semantics of the virtual memory system on modern operating +systems. As a side effect, the startup time is also reduced. This is technique +is exploited by Phusion Passenger's 'smart' and 'smart-lv2' spawn methods. + +==== How it works ==== + +When the 'smart-lv2' spawn method is being used, Phusion Passenger will first +create a so-called 'ApplicationSpawner server' process. This process loads the +entire Rails application along with the Rails framework, by loading +'environment.rb'. Then, whenever Phusion Passenger needs a new worker process, +it will instruct the ApplicationSpawner server to do so. The ApplicationSpawner +server will create a worker new process +that reuses the already loaded Rails application/framework. Creating a worker +process through an already running ApplicationSpawner server is very fast, about +10 times faster than loading the Rails application/framework from scratch. If +the Ruby interpreter is copy-on-write friendly (that is, if you're running +<>) then all created worker +processes will share as much common +memory as possible. That is, they will all share the same application and Rails +framework code. + +image:images/smart-lv2.png[] + +'Figure: Worker processes and the smart-lv2 spawn method. All worker processes, +as well as the ApplicationSpawner, share the same application code and Rails +framework code.' + +The 'smart' spawn method goes even further, by caching the Rails framework in +another process called the 'FrameworkSpawner server'. This process only loads +the Rails framework, not the application. When a FrameworkSpawner server is +instructed to create a new worker process, it will create a new +ApplicationSpawner to which the instruction will be delegated. All those +ApplicationSpawner servers, as well as all worker processes created by those +ApplicationSpawner servers, will share the same Rails framework code. + +The 'smart-lv2' method allows different worker processes that belong to the same +application to share memory. The 'smart' method allows different worker +processes - that happen to use the same Rails version - to share memory, even if +they don't belong to the same application. + +Notes: + +- Vendored Rails frameworks cannot be shared by different applications, even if + both vendored Rails frameworks are the same version. So for efficiency reasons + we don't recommend vendoring Rails. +- ApplicationSpawner and FrameworkSpawner servers have an idle timeout just + like worker processes. If an ApplicationSpawner/FrameworkSpawner server hasn't + been instructed to do anything for a while, it will be shutdown in order to + conserve memory. This idle timeout is configurable. + +==== Summary of benefits ==== + +Suppose that Phusion Passenger needs a new worker process for an application +that uses Rails 2.2.1. + +- If the 'smart-lv2' spawning method is used, and an ApplicationSpawner server + for this application is already running, then worker process creation time is + about 10 times faster than conservative spawning. This worker process will also + share application and Rails framework code memory with the ApplicationSpawner + server and the worker processes that had been spawned by this ApplicationSpawner + server. +- If the 'smart' spawning method is used, and a FrameworkSpawner server for + Rails 2.2.1 is already running, but no ApplicationSpawner server for this + application is running, then worker process creation time is about 2 times + faster than conservative spawning. If there is an ApplicationSpawner server + for this application running, then worker process creation time is about 10 + times faster. This worker process will also share application and Rails + framework code memory with the ApplicationSpawner and FrameworkSpawner + servers. + +You could compare ApplicationSpawner and FrameworkSpawner servers with stem +cells, that have the ability to quickly change into more specific cells (worker +process). + +In practice, the smart spawning methods could mean a memory saving of about 33%, +assuming that your Ruby interpreter is <>. + +Of course, smart spawning is not without gotchas. But if you understand the +gotchas you can easily reap the benefits of smart spawning. + +=== Smart spawning gotcha #1: unintential file descriptor sharing === + +Because worker processes are created by forking from an ApplicationSpawner +server, it will share all file descriptors that are opened by the +ApplicationSpawner server. (This is part of the semantics of the Unix +'fork()' system call. You might want to Google it if you're not familiar with +it.) A file descriptor is a handle which can be an opened file, an opened socket +connection, a pipe, etc. If different worker processes write to such a file +descriptor at the same time, then their write calls will be interleaved, which +may potentially cause problems. + +The problem commonly involves socket connections that are unintentially being +shared. You can fix it by closing and reestablishing the connection when Phusion +Passenger is creating a new worker process. Phusion Passenger provides the API +call `PhusionPassenger.on_event(:starting_worker_process)` to do so. So you +could insert the following code in your 'environment.rb': + +[source, ruby] +----------------------------------------- +if defined?(PhusionPassenger) + PhusionPassenger.on_event(:starting_worker_process) do |forked| + if forked + # We're in smart spawning mode. + ... code to reestablish socket connections here ... + else + # We're in conservative spawning mode. We don't need to do anything. + end + end +end +----------------------------------------- + +Note that Phusion Passenger automatically reestablishes the connection to the +database upon creating a new worker process, which is why you normally do not +encounter any database issues when using smart spawning mode. + +==== Example 1: Memcached connection sharing (harmful) ==== + +Suppose we have a Rails application that connects to a Memcached server in +'environment.rb'. This causes the ApplicationSpawner to have a socket connection +(file descriptor) to the Memcached server, as shown in the following figure: + + +--------------------+ + | ApplicationSpawner |-----------[Memcached server] + +--------------------+ + +Phusion Passenger then proceeds with creating a new Rails worker process, which +is to process incoming HTTP requests. The result will look like this: + + +--------------------+ + | ApplicationSpawner |------+----[Memcached server] + +--------------------+ | + | + +--------------------+ | + | Worker process 1 |-----/ + +--------------------+ + +Since a 'fork()' makes a (virtual) complete copy of a process, all its file +descriptors will be copied as well. What we see here is that ApplicationSpawner +and Worker process 1 both share the same connection to Memcached. + +Now supposed that your site gets Slashdotted and Phusion Passenger needs to +spawn another worker process. It does so by forking ApplicationSpawner. The +result is now as follows: + + +--------------------+ + | ApplicationSpawner |------+----[Memcached server] + +--------------------+ | + | + +--------------------+ | + | Worker process 1 |-----/| + +--------------------+ | + | + +--------------------+ | + | Worker process 2 |-----/ + +--------------------+ + +As you can see, Worker process 1 and Worker process 2 have the same Memcache +connection. + +Suppose that users Joe and Jane visit your website at the same time. Joe's +request is handled by Worker process 1, and Jane's request is handled by Worker +process 2. Both worker processes want to fetch something from Memcached. Suppose +that in order to do that, both handlers need to send a "FETCH" command to Memcached. + +But suppose that, after worker process 1 having only sent "FE", a context switch +occurs, and worker process 2 starts sending a "FETCH" command to Memcached as +well. If worker process 2 succeeds in sending only one bye, 'F', then Memcached +will receive a command which begins with "FEF", a command that it does not +recognize. In other words: the data from both handlers get interleaved. And thus +Memcached is forced to handle this as an error. + +This problem can be solved by reestablishing the connection to Memcached after forking: + + +--------------------+ + | ApplicationSpawner |------+----[Memcached server] + +--------------------+ | | + | | + +--------------------+ | | + | Worker process 1 |-----/| | + +--------------------+ | | <--- created this + X | new + | connection + X <-- closed this | + +--------------------+ | old | + | Worker process 2 |-----/ connection | + +--------------------+ | + | | + +-------------------------------------+ + +Worker process 2 now has its own, separate communication channel with Memcached. +The code in 'environment.rb' looks like this: + +[source, ruby] +----------------------------------------- +if defined?(PhusionPassenger) + PhusionPassenger.on_event(:starting_worker_process) do |forked| + if forked + # We're in smart spawning mode. + reestablish_connection_to_memcached + else + # We're in conservative spawning mode. We don't need to do anything. + end + end +end +----------------------------------------- + +==== Example 2: Log file sharing (not harmful) ==== + +There are also cases in which unintential file descriptor sharing is not harmful. +One such case is log file file descriptor sharing. Even if two processes write +to the log file at the same time, the worst thing that can happen is that the +data in the log file is interleaved. + +To guarantee that the data written to the log file is never interleaved, you +must synchronize write access via an inter-process synchronization mechanism, +such as file locks. Reopening the log file, like you would have done in the +Memcached example, doesn't help. + +=== Smart spawning gotcha #2: the need to revive threads === + +Another part of the 'fork()' system call's semantics is the fact that threads +disappear after a fork call. So if you've created any threads in environment.rb, +then those threads will no longer be running in newly created worker process. +You need to revive them when a new worker process is created. Use the +`:starting_worker_process` event that Phusion Passenger provides, like this: + +[source, ruby] +----------------------------------------- +if defined?(PhusionPassenger) + PhusionPassenger.on_event(:starting_worker_process) do |forked| + if forked + # We're in smart spawning mode. + ... code to revive threads here ... + else + # We're in conservative spawning mode. We don't need to do anything. + end + end +end +----------------------------------------- + +=== Smart spawning gotcha #3: code load order === + +This gotcha is only applicable to the 'smart' spawn method, not the 'smart-lv2' +spawn method. + +If your application expects the Rails framework to be not loaded during the +beginning of 'environment.rb', then it can cause problems when an +ApplicationSpawner is created from a FrameworkSpawner, which already has the +Rails framework loaded. The most common case is when applications try to patch +Rails by dropping a modified file that has the same name as Rails's own file, +in a path that comes earlier in the Ruby search path. + +For example, suppose that we have an application which has a patched version +of 'active_record/base.rb' located in 'RAILS_ROOT/lib/patches', and +'RAILS_ROOT/lib/patches' comes first in the Ruby load path. When conservative +spawning is used, the patched version of 'base.rb' is properly loaded. When +'smart' (not 'smart-lv2') spawning is used, the original 'base.rb' is used +because it was already loaded, so a subsequent `require "active_record/base"` +has no effect. diff --git a/doc/images/conservative_spawning.png b/doc/images/conservative_spawning.png new file mode 100644 index 00000000..6cab5602 Binary files /dev/null and b/doc/images/conservative_spawning.png differ diff --git a/doc/images/conservative_spawning.svg b/doc/images/conservative_spawning.svg new file mode 100644 index 00000000..d7a62b4e --- /dev/null +++ b/doc/images/conservative_spawning.svg @@ -0,0 +1,248 @@ + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + Application code + + Rails framework code + + Other memory + Worker process 1 + + + Application code + + Rails framework code + + Other memory + Worker process 2 + + diff --git a/doc/images/smart-lv2.png b/doc/images/smart-lv2.png new file mode 100644 index 00000000..4203ce64 Binary files /dev/null and b/doc/images/smart-lv2.png differ diff --git a/doc/images/smart-lv2.svg b/doc/images/smart-lv2.svg new file mode 100644 index 00000000..337ac461 --- /dev/null +++ b/doc/images/smart-lv2.svg @@ -0,0 +1,320 @@ + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + Application code + + Rails framework code + + + + + Other memory + Worker process 1 + + + + + Other memory + Worker process 2 + + + + + Other memory + ApplicationSpawner + + + + + + diff --git a/ext/apache2/Application.h b/ext/apache2/Application.h index 8fe7938c..59d50a04 100644 --- a/ext/apache2/Application.h +++ b/ext/apache2/Application.h @@ -22,11 +22,15 @@ #include #include +#include +#include #include +#include #include #include #include +#include #include #include #include @@ -35,6 +39,7 @@ #include "MessageChannel.h" #include "Exceptions.h" #include "Logging.h" +#include "Utils.h" namespace Passenger { @@ -105,6 +110,7 @@ class Application { * @throws boost::thread_interrupted */ virtual void sendHeaders(const char *headers, unsigned int size) { + TRACE_POINT(); int stream = getStream(); if (stream == -1) { throw IOException("Cannot write headers to the request handler " @@ -112,9 +118,10 @@ class Application { } try { MessageChannel(stream).writeScalar(headers, size); - } catch (const SystemException &e) { - throw SystemException("An error occured while writing headers " - "to the request handler", e.code()); + } catch (SystemException &e) { + e.setBriefMessage("An error occured while writing headers " + "to the request handler"); + throw; } } @@ -144,6 +151,7 @@ class Application { * @throws boost::thread_interrupted */ virtual void sendBodyBlock(const char *block, unsigned int size) { + TRACE_POINT(); int stream = getStream(); if (stream == -1) { throw IOException("Cannot write request body block to the " @@ -152,9 +160,10 @@ class Application { } try { MessageChannel(stream).writeRaw(block, size); - } catch (const SystemException &e) { - throw SystemException("An error occured while sending the " - "request body to the request handler", e.code()); + } catch (SystemException &e) { + e.setBriefMessage("An error occured while sending the " + "request body to the request handler"); + throw; } } @@ -167,6 +176,28 @@ class Application { */ virtual int getStream() const = 0; + /** + * Set the timeout value for reading data from the I/O stream. + * If no data can be read within the timeout period, then the + * read call will fail with error EAGAIN or EWOULDBLOCK. + * + * @param msec The timeout, in milliseconds. If 0 is given, + * there will be no timeout. + * @throws SystemException Cannot set the timeout. + */ + virtual void setReaderTimeout(unsigned int msec) = 0; + + /** + * Set the timeout value for writing data from the I/O stream. + * If no data can be written within the timeout period, then the + * write call will fail with error EAGAIN or EWOULDBLOCK. + * + * @param msec The timeout, in milliseconds. If 0 is given, + * there will be no timeout. + * @throws SystemException Cannot set the timeout. + */ + virtual void setWriterTimeout(unsigned int msec) = 0; + /** * Indicate that we don't want to read data anymore from the I/O stream. * Calling this method after closeStream() is called will have no effect. @@ -225,6 +256,7 @@ class Application { } virtual ~StandardSession() { + TRACE_POINT(); closeStream(); closeCallback(); } @@ -233,9 +265,18 @@ class Application { return fd; } + virtual void setReaderTimeout(unsigned int msec) { + MessageChannel(fd).setReadTimeout(msec); + } + + virtual void setWriterTimeout(unsigned int msec) { + MessageChannel(fd).setWriteTimeout(msec); + } + virtual void shutdownReader() { + TRACE_POINT(); if (fd != -1) { - int ret = InterruptableCalls::shutdown(fd, SHUT_RD); + int ret = syscalls::shutdown(fd, SHUT_RD); if (ret == -1) { throw SystemException("Cannot shutdown the writer stream", errno); @@ -244,8 +285,9 @@ class Application { } virtual void shutdownWriter() { + TRACE_POINT(); if (fd != -1) { - int ret = InterruptableCalls::shutdown(fd, SHUT_WR); + int ret = syscalls::shutdown(fd, SHUT_WR); if (ret == -1) { throw SystemException("Cannot shutdown the writer stream", errno); @@ -254,8 +296,9 @@ class Application { } virtual void closeStream() { + TRACE_POINT(); if (fd != -1) { - int ret = InterruptableCalls::close(fd); + int ret = syscalls::close(fd); if (ret == -1) { throw SystemException("Cannot close the session stream", errno); @@ -276,8 +319,88 @@ class Application { string appRoot; pid_t pid; string listenSocketName; - bool usingAbstractNamespace; + string listenSocketType; int ownerPipe; + + SessionPtr connectToUnixServer(const function &closeCallback) const { + TRACE_POINT(); + int fd, ret; + + do { + fd = socket(PF_UNIX, SOCK_STREAM, 0); + } while (fd == -1 && errno == EINTR); + if (fd == -1) { + throw SystemException("Cannot create a new unconnected Unix socket", errno); + } + + struct sockaddr_un addr; + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, listenSocketName.c_str(), sizeof(addr.sun_path)); + addr.sun_path[sizeof(addr.sun_path) - 1] = '\0'; + do { + ret = ::connect(fd, (const sockaddr *) &addr, sizeof(addr)); + } while (ret == -1 && errno == EINTR); + if (ret == -1) { + int e = errno; + string message("Cannot connect to Unix socket '"); + message.append(listenSocketName); + message.append("'"); + do { + ret = close(fd); + } while (ret == -1 && errno == EINTR); + throw SystemException(message, e); + } + + return ptr(new StandardSession(pid, closeCallback, fd)); + } + + SessionPtr connectToTcpServer(const function &closeCallback) const { + TRACE_POINT(); + int fd, ret; + vector args; + + split(listenSocketName, ':', args); + if (args.size() != 2 || atoi(args[1]) == 0) { + throw IOException("Invalid TCP/IP address '" + listenSocketName + "'"); + } + + struct addrinfo hints, *res; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = PF_INET; + hints.ai_socktype = SOCK_STREAM; + ret = getaddrinfo(args[0].c_str(), args[1].c_str(), &hints, &res); + if (ret != 0) { + int e = errno; + throw IOException("Cannot resolve address '" + listenSocketName + + "': " + gai_strerror(e)); + } + + do { + fd = socket(PF_INET, SOCK_STREAM, 0); + } while (fd == -1 && errno == EINTR); + if (fd == -1) { + freeaddrinfo(res); + throw SystemException("Cannot create a new unconnected TCP socket", errno); + } + + do { + ret = ::connect(fd, res->ai_addr, res->ai_addrlen); + } while (ret == -1 && errno == EINTR); + freeaddrinfo(res); + if (ret == -1) { + int e = errno; + string message("Cannot connect to TCP server '"); + message.append(listenSocketName); + message.append("'"); + do { + ret = close(fd); + } while (ret == -1 && errno == EINTR); + throw SystemException(message, e); + } + + return ptr(new StandardSession(pid, closeCallback, fd)); + } public: /** @@ -288,23 +411,23 @@ class Application { * This must be a valid directory, but the path does not have to be absolute. * @param pid The process ID of this application instance. * @param listenSocketName The name of the listener socket of this application instance. - * @param usingAbstractNamespace Whether listenSocketName refers to a Unix - * socket on the abstract namespace. Note that listenSocketName must not - * contain the leading null byte, even if it's an abstract namespace socket. + * @param listenSocketType The type of the listener socket, e.g. "unix" for Unix + * domain sockets. * @param ownerPipe The owner pipe of this application instance. * @post getAppRoot() == theAppRoot && getPid() == pid */ Application(const string &theAppRoot, pid_t pid, const string &listenSocketName, - bool usingAbstractNamespace, int ownerPipe) { + const string &listenSocketType, int ownerPipe) { appRoot = theAppRoot; this->pid = pid; this->listenSocketName = listenSocketName; - this->usingAbstractNamespace = usingAbstractNamespace; + this->listenSocketType = listenSocketType; this->ownerPipe = ownerPipe; P_TRACE(3, "Application " << this << ": created."); } virtual ~Application() { + TRACE_POINT(); int ret; if (ownerPipe != -1) { @@ -312,7 +435,7 @@ class Application { ret = close(ownerPipe); } while (ret == -1 && errno == EINTR); } - if (!usingAbstractNamespace) { + if (listenSocketType == "unix") { do { ret = unlink(listenSocketName.c_str()); } while (ret == -1 && errno == EINTR); @@ -384,36 +507,14 @@ class Application { * @throws IOException Something went wrong during the connection process. */ SessionPtr connect(const function &closeCallback) const { - int fd, ret; - - do { - fd = socket(PF_UNIX, SOCK_STREAM, 0); - } while (fd == -1 && errno == EINTR); - if (fd == -1) { - throw SystemException("Cannot create a new unconnected Unix socket", errno); - } - - struct sockaddr_un addr; - addr.sun_family = AF_UNIX; - if (usingAbstractNamespace) { - strncpy(addr.sun_path + 1, listenSocketName.c_str(), sizeof(addr.sun_path) - 1); - addr.sun_path[0] = '\0'; + TRACE_POINT(); + if (listenSocketType == "unix") { + return connectToUnixServer(closeCallback); + } else if (listenSocketType == "tcp") { + return connectToTcpServer(closeCallback); } else { - strncpy(addr.sun_path, listenSocketName.c_str(), sizeof(addr.sun_path)); - } - addr.sun_path[sizeof(addr.sun_path) - 1] = '\0'; - do { - ret = ::connect(fd, (const sockaddr *) &addr, sizeof(addr)); - } while (ret == -1 && errno == EINTR); - if (ret == -1) { - int e = errno; - string message("Cannot connect to Unix socket '"); - message.append(listenSocketName); - message.append("' on the abstract namespace"); - throw SystemException(message, e); + throw IOException("Unsupported socket type '" + listenSocketType + "'"); } - - return ptr(new StandardSession(pid, closeCallback, fd)); } }; diff --git a/ext/apache2/ApplicationPool.h b/ext/apache2/ApplicationPool.h index 8e0540fb..6e696f1d 100644 --- a/ext/apache2/ApplicationPool.h +++ b/ext/apache2/ApplicationPool.h @@ -24,6 +24,7 @@ #include #include "Application.h" +#include "PoolOptions.h" namespace Passenger { @@ -91,26 +92,30 @@ class ApplicationPool { virtual ~ApplicationPool() {}; /** - * Open a new session with the application specified by appRoot. + * Checks whether this ApplicationPool object is still connected to the + * ApplicationPool server. + * + * If that's not the case, then one should reconnect to the ApplicationPool server. + * + * This method is only meaningful for instances of type ApplicationPoolServer::Client. + * The default implementation always returns true. + */ + virtual bool connected() const { + return true; + } + + /** + * Open a new session with the application specified by PoolOptions.appRoot. * See the class description for ApplicationPool, as well as Application::connect(), * on how to use the returned session object. * * Internally, this method may either spawn a new application instance, or use * an existing one. * - * If lowerPrivilege is true, then any newly spawned application - * instances will have lower privileges. See SpawnManager::SpawnManager()'s - * description of lowerPrivilege and lowestUser for details. - * - * @param appRoot The application root of a RoR application, i.e. the folder that - * contains 'app/', 'public/', 'config/', etc. This must be a valid - * directory, but does not have to be an absolute path. - * @param lowerPrivilege Whether to lower the application's privileges. - * @param lowestUser The user to fallback to if lowering privilege fails. - * @param environment The RAILS_ENV/RACK_ENV environment that should be used. May not be empty. - * @param spawnMethod The spawn method to use. Either "smart" or "conservative". - * See the Ruby class SpawnManager for details. - * @param appType The application type. Either "rails" or "rack". + * @param options An object containing information on which application to open + * a session with, as well as spawning details. Spawning details will be used + * if the pool decides that spawning a new application instance is necessary. + * See SpawnManager and PoolOptions for details. * @return A session object. * @throw SpawnException An attempt was made to spawn a new application instance, but that attempt failed. * @throw BusyException The application pool is too busy right now, and cannot @@ -123,9 +128,14 @@ class ApplicationPool { * get("/home/../home/foo"), then ApplicationPool will think * they're 2 different applications, and thus will spawn 2 application instances. */ - virtual Application::SessionPtr get(const string &appRoot, bool lowerPrivilege = true, - const string &lowestUser = "nobody", const string &environment = "production", - const string &spawnMethod = "smart", const string &appType = "rails") = 0; + virtual Application::SessionPtr get(const PoolOptions &options) = 0; + + /** + * Convenience shortcut for calling get() with default spawn options. + */ + virtual Application::SessionPtr get(const string &appRoot) { + return get(PoolOptions(appRoot)); + } /** * Clear all application instances that are currently in the pool. @@ -173,18 +183,6 @@ class ApplicationPool { */ virtual void setMaxPerApp(unsigned int max) = 0; - /** - * Sets whether to use a global queue instead of a per-backend process - * queue. If enabled, when all backend processes are active, get() will - * wait until there's at least one backend process that's idle, instead - * of queuing the request into a random process's private queue. - * This is especially useful if a website has one or more long-running - * requests. - * - * Defaults to false. - */ - virtual void setUseGlobalQueue(bool value) = 0; - /** * Get the process ID of the spawn server that is used. * diff --git a/ext/apache2/ApplicationPoolServer.h b/ext/apache2/ApplicationPoolServer.h index c4e3f10a..b2985b20 100644 --- a/ext/apache2/ApplicationPoolServer.h +++ b/ext/apache2/ApplicationPoolServer.h @@ -22,6 +22,8 @@ #include #include +#include +#include #include #include @@ -40,12 +42,12 @@ #include "Application.h" #include "Exceptions.h" #include "Logging.h" -#include "System.h" namespace Passenger { using namespace std; using namespace boost; +using namespace oxt; /** @@ -140,16 +142,31 @@ class ApplicationPoolServer { /** * The socket connection to the ApplicationPool server, as was * established by ApplicationPoolServer::connect(). + * + * The value may be -1, which indicates that the connection has + * been closed. */ int server; - mutex lock; + mutable boost::mutex lock; ~SharedData() { + TRACE_POINT(); + if (server != -1) { + disconnect(); + } + } + + /** + * Disconnect from the ApplicationPool server. + */ + void disconnect() { + TRACE_POINT(); int ret; do { ret = close(server); } while (ret == -1 && errno == EINTR); + server = -1; } }; @@ -174,7 +191,7 @@ class ApplicationPoolServer { virtual ~RemoteSession() { closeStream(); - mutex::scoped_lock(data->lock); + boost::mutex::scoped_lock(data->lock); MessageChannel(data->server).write("close", toString(id).c_str(), NULL); } @@ -182,9 +199,17 @@ class ApplicationPoolServer { return fd; } + virtual void setReaderTimeout(unsigned int msec) { + MessageChannel(fd).setReadTimeout(msec); + } + + virtual void setWriterTimeout(unsigned int msec) { + MessageChannel(fd).setWriteTimeout(msec); + } + virtual void shutdownReader() { if (fd != -1) { - int ret = InterruptableCalls::shutdown(fd, SHUT_RD); + int ret = syscalls::shutdown(fd, SHUT_RD); if (ret == -1) { throw SystemException("Cannot shutdown the writer stream", errno); @@ -194,7 +219,7 @@ class ApplicationPoolServer { virtual void shutdownWriter() { if (fd != -1) { - int ret = InterruptableCalls::shutdown(fd, SHUT_WR); + int ret = syscalls::shutdown(fd, SHUT_WR); if (ret == -1) { throw SystemException("Cannot shutdown the writer stream", errno); @@ -204,7 +229,7 @@ class ApplicationPoolServer { virtual void closeStream() { if (fd != -1) { - int ret = InterruptableCalls::close(fd); + int ret = syscalls::close(fd); if (ret == -1) { throw SystemException("Cannot close the session stream", errno); @@ -246,112 +271,167 @@ class ApplicationPoolServer { data->server = sock; } + virtual bool connected() const { + boost::mutex::scoped_lock(data->lock); + return data->server != -1; + } + virtual void clear() { MessageChannel channel(data->server); - mutex::scoped_lock l(data->lock); - channel.write("clear", NULL); + boost::mutex::scoped_lock l(data->lock); + try { + channel.write("clear", NULL); + } catch (...) { + data->disconnect(); + throw; + } } virtual void setMaxIdleTime(unsigned int seconds) { MessageChannel channel(data->server); - mutex::scoped_lock l(data->lock); - channel.write("setMaxIdleTime", toString(seconds).c_str(), NULL); + boost::mutex::scoped_lock l(data->lock); + try { + channel.write("setMaxIdleTime", toString(seconds).c_str(), NULL); + } catch (...) { + data->disconnect(); + throw; + } } virtual void setMax(unsigned int max) { MessageChannel channel(data->server); - mutex::scoped_lock l(data->lock); - channel.write("setMax", toString(max).c_str(), NULL); + boost::mutex::scoped_lock l(data->lock); + try { + channel.write("setMax", toString(max).c_str(), NULL); + } catch (...) { + data->disconnect(); + throw; + } } virtual unsigned int getActive() const { MessageChannel channel(data->server); - mutex::scoped_lock l(data->lock); + boost::mutex::scoped_lock l(data->lock); vector args; - channel.write("getActive", NULL); - channel.read(args); - return atoi(args[0].c_str()); + try { + channel.write("getActive", NULL); + channel.read(args); + return atoi(args[0].c_str()); + } catch (...) { + data->disconnect(); + throw; + } } virtual unsigned int getCount() const { MessageChannel channel(data->server); - mutex::scoped_lock l(data->lock); + boost::mutex::scoped_lock l(data->lock); vector args; - channel.write("getCount", NULL); - channel.read(args); - return atoi(args[0].c_str()); + try { + channel.write("getCount", NULL); + channel.read(args); + return atoi(args[0].c_str()); + } catch (...) { + data->disconnect(); + throw; + } } virtual void setMaxPerApp(unsigned int max) { - MessageChannel channel(data->server); - mutex::scoped_lock l(data->lock); - channel.write("setMaxPerApp", toString(max).c_str(), NULL); - } - - virtual void setUseGlobalQueue(bool value) { MessageChannel channel(data->server); boost::mutex::scoped_lock l(data->lock); - channel.write("setUseGlobalQueue", value ? "true" : "false", NULL); + try { + channel.write("setMaxPerApp", toString(max).c_str(), NULL); + } catch (...) { + data->disconnect(); + throw; + } } virtual pid_t getSpawnServerPid() const { this_thread::disable_syscall_interruption dsi; MessageChannel channel(data->server); - mutex::scoped_lock l(data->lock); + boost::mutex::scoped_lock l(data->lock); vector args; - channel.write("getSpawnServerPid", NULL); - channel.read(args); - return atoi(args[0].c_str()); - } - - virtual Application::SessionPtr get( - const string &appRoot, - bool lowerPrivilege = true, - const string &lowestUser = "nobody", - const string &environment = "production", - const string &spawnMethod = "smart", - const string &appType = "rails" - ) { + try { + channel.write("getSpawnServerPid", NULL); + channel.read(args); + return atoi(args[0].c_str()); + } catch (...) { + data->disconnect(); + throw; + } + } + + virtual Application::SessionPtr get(const PoolOptions &options) { this_thread::disable_syscall_interruption dsi; + TRACE_POINT(); + MessageChannel channel(data->server); - mutex::scoped_lock l(data->lock); + boost::mutex::scoped_lock l(data->lock); vector args; int stream; bool result; try { - channel.write("get", appRoot.c_str(), - (lowerPrivilege) ? "true" : "false", - lowestUser.c_str(), - environment.c_str(), - spawnMethod.c_str(), - appType.c_str(), - NULL); - } catch (const SystemException &) { - throw IOException("The ApplicationPool server exited unexpectedly."); + vector args; + + args.push_back("get"); + options.toVector(args); + channel.write(args); + } catch (const SystemException &e) { + UPDATE_TRACE_POINT(); + data->disconnect(); + + string message("Could not send data to the ApplicationPool server: "); + message.append(e.brief()); + throw SystemException(message, e.code()); } try { + UPDATE_TRACE_POINT(); result = channel.read(args); } catch (const SystemException &e) { + UPDATE_TRACE_POINT(); + data->disconnect(); throw SystemException("Could not read a message from " "the ApplicationPool server", e.code()); } if (!result) { + UPDATE_TRACE_POINT(); + data->disconnect(); throw IOException("The ApplicationPool server unexpectedly " "closed the connection."); } if (args[0] == "ok") { - stream = channel.readFileDescriptor(); + UPDATE_TRACE_POINT(); + pid_t pid = (pid_t) atol(args[1]); + int sessionID = atoi(args[2]); + + try { + stream = channel.readFileDescriptor(); + } catch (...) { + UPDATE_TRACE_POINT(); + data->disconnect(); + throw; + } + return ptr(new RemoteSession(dataSmartPointer, - atoi(args[1]), atoi(args[2]), stream)); + pid, sessionID, stream)); } else if (args[0] == "SpawnException") { + UPDATE_TRACE_POINT(); if (args[2] == "true") { string errorPage; - if (!channel.readScalar(errorPage)) { + try { + result = channel.readScalar(errorPage); + } catch (...) { + data->disconnect(); + throw; + } + if (!result) { throw IOException("The ApplicationPool server " "unexpectedly closed the connection."); } @@ -360,10 +440,15 @@ class ApplicationPoolServer { throw SpawnException(args[1]); } } else if (args[0] == "BusyException") { + UPDATE_TRACE_POINT(); throw BusyException(args[1]); } else if (args[0] == "IOException") { + UPDATE_TRACE_POINT(); + data->disconnect(); throw IOException(args[1]); } else { + UPDATE_TRACE_POINT(); + data->disconnect(); throw IOException("The ApplicationPool server returned " "an unknown message: " + toString(args)); } @@ -408,12 +493,13 @@ class ApplicationPoolServer { * @post serverSocket == -1 && serverPid == 0 */ void shutdownServer() { + TRACE_POINT(); this_thread::disable_syscall_interruption dsi; int ret; time_t begin; bool done = false; - InterruptableCalls::close(serverSocket); + syscalls::close(serverSocket); if (!statusReportFIFO.empty()) { do { ret = unlink(statusReportFIFO.c_str()); @@ -422,28 +508,28 @@ class ApplicationPoolServer { P_TRACE(2, "Waiting for existing ApplicationPoolServerExecutable (PID " << serverPid << ") to exit..."); - begin = InterruptableCalls::time(NULL); - while (!done && InterruptableCalls::time(NULL) < begin + 5) { + begin = syscalls::time(NULL); + while (!done && syscalls::time(NULL) < begin + 5) { /* * Some Apache modules fork(), but don't close file descriptors. * mod_wsgi is one such example. Because of that, closing serverSocket * won't always cause the ApplicationPool server to exit. So we send it a * signal. */ - InterruptableCalls::kill(serverPid, SIGINT); + syscalls::kill(serverPid, SIGINT); - ret = InterruptableCalls::waitpid(serverPid, NULL, WNOHANG); + ret = syscalls::waitpid(serverPid, NULL, WNOHANG); done = ret > 0 || ret == -1; if (!done) { - InterruptableCalls::usleep(100000); + syscalls::usleep(100000); } } if (done) { P_TRACE(2, "ApplicationPoolServerExecutable exited."); } else { P_DEBUG("ApplicationPoolServerExecutable not exited in time. Killing it..."); - InterruptableCalls::kill(serverPid, SIGTERM); - InterruptableCalls::waitpid(serverPid, NULL, 0); + syscalls::kill(serverPid, SIGTERM); + syscalls::waitpid(serverPid, NULL, 0); } serverSocket = -1; @@ -459,6 +545,7 @@ class ApplicationPoolServer { * @throw SystemException Something went wrong. */ void restartServer() { + TRACE_POINT(); int fds[2]; pid_t pid; @@ -466,13 +553,13 @@ class ApplicationPoolServer { shutdownServer(); } - if (InterruptableCalls::socketpair(AF_UNIX, SOCK_STREAM, 0, fds) == -1) { + if (syscalls::socketpair(AF_UNIX, SOCK_STREAM, 0, fds) == -1) { throw SystemException("Cannot create a Unix socket pair", errno); } createStatusReportFIFO(); - pid = InterruptableCalls::fork(); + pid = syscalls::fork(); if (pid == 0) { // Child process. dup2(fds[0], SERVER_SOCKET_FD); @@ -495,18 +582,20 @@ class ApplicationPoolServer { m_rubyCommand.c_str(), m_user.c_str(), statusReportFIFO.c_str(), - NULL); + (char *) 0); int e = errno; - fprintf(stderr, "*** Passenger ERROR: Cannot execute %s: %s (%d)\n", + fprintf(stderr, "*** Passenger ERROR (%s:%d):\n" + "Cannot execute %s: %s (%d)\n", + __FILE__, __LINE__, m_serverExecutable.c_str(), strerror(e), e); fflush(stderr); _exit(1); } else if (pid == -1) { // Error. - InterruptableCalls::close(fds[0]); - InterruptableCalls::close(fds[1]); + syscalls::close(fds[0]); + syscalls::close(fds[1]); throw SystemException("Cannot create a new process", errno); } else { // Parent process. - InterruptableCalls::close(fds[0]); + syscalls::close(fds[0]); serverSocket = fds[1]; int flags = fcntl(serverSocket, F_GETFD); @@ -519,14 +608,24 @@ class ApplicationPoolServer { } void createStatusReportFIFO() { + TRACE_POINT(); char filename[PATH_MAX]; int ret; + mode_t permissions; + + createPassengerTempDir(); - snprintf(filename, sizeof(filename), "/tmp/passenger_status.%d.fifo", - getpid()); + if (m_user.empty()) { + permissions = S_IRUSR | S_IWUSR; + } else { + permissions = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH; + } + + snprintf(filename, sizeof(filename), "%s/status.fifo", + getPassengerTempDir().c_str()); filename[PATH_MAX - 1] = '\0'; do { - ret = mkfifo(filename, S_IRUSR | S_IWUSR); + ret = mkfifo(filename, permissions); } while (ret == -1 && errno == EINTR); if (ret == -1 && errno != EEXIST) { int e = errno; @@ -536,6 +635,12 @@ class ApplicationPoolServer { statusReportFIFO = ""; } else { statusReportFIFO = filename; + + // It seems that the permissions passed to mkfifo() + // aren't respected, so here we chmod the file. + do { + ret = chmod(filename, permissions); + } while (ret == -1 && errno == EINTR); } } @@ -572,6 +677,7 @@ class ApplicationPoolServer { m_logFile(logFile), m_rubyCommand(rubyCommand), m_user(user) { + TRACE_POINT(); serverSocket = -1; serverPid = 0; this_thread::disable_syscall_interruption dsi; @@ -579,7 +685,9 @@ class ApplicationPoolServer { } ~ApplicationPoolServer() { + TRACE_POINT(); if (serverSocket != -1) { + UPDATE_TRACE_POINT(); this_thread::disable_syscall_interruption dsi; shutdownServer(); } @@ -618,6 +726,7 @@ class ApplicationPoolServer { * @throws IOException Something went wrong. */ ApplicationPoolPtr connect() { + TRACE_POINT(); try { this_thread::disable_syscall_interruption dsi; MessageChannel channel(serverSocket); @@ -652,6 +761,7 @@ class ApplicationPoolServer { * before calling detach(). */ void detach() { + TRACE_POINT(); int ret; do { ret = close(serverSocket); diff --git a/ext/apache2/ApplicationPoolServerExecutable.cpp b/ext/apache2/ApplicationPoolServerExecutable.cpp index fbdb5897..c0b53f90 100644 --- a/ext/apache2/ApplicationPoolServerExecutable.cpp +++ b/ext/apache2/ApplicationPoolServerExecutable.cpp @@ -36,13 +36,20 @@ #include #include +#include +#include +#include + #include #include #include #include #include #include +#include +#include #include +#include #include #include #include @@ -51,12 +58,12 @@ #include "StandardApplicationPool.h" #include "Application.h" #include "Logging.h" -#include "System.h" #include "Exceptions.h" using namespace boost; using namespace std; +using namespace oxt; using namespace Passenger; class Server; @@ -77,16 +84,19 @@ class Server { int serverSocket; StandardApplicationPool pool; set clients; - mutex lock; + boost::mutex lock; string statusReportFIFO; - shared_ptr statusReportThread; + shared_ptr statusReportThread; + string user; void statusReportThreadMain() { + TRACE_POINT(); try { while (!this_thread::interruption_requested()) { struct stat buf; int ret; + UPDATE_TRACE_POINT(); do { ret = stat(statusReportFIFO.c_str(), &buf); } while (ret == -1 && errno == EINTR); @@ -96,17 +106,32 @@ class Server { break; } - FILE *f = InterruptableCalls::fopen(statusReportFIFO.c_str(), "w"); + UPDATE_TRACE_POINT(); + FILE *f = syscalls::fopen(statusReportFIFO.c_str(), "w"); if (f == NULL) { + int e = errno; + P_ERROR("Cannot open status report FIFO " << + statusReportFIFO << ": " << + strerror(e) << " (" << e << ")"); break; } - string report(pool.toString()); - fwrite(report.c_str(), 1, report.size(), f); - InterruptableCalls::fclose(f); + UPDATE_TRACE_POINT(); + MessageChannel channel(fileno(f)); + string report; + report.append("----------- Backtraces -----------\n"); + report.append(oxt::thread::all_backtraces()); + report.append("\n\n"); + report.append(pool.toString()); - // Prevent sending too much data at once. - sleep(1); + UPDATE_TRACE_POINT(); + try { + channel.writeScalar(report); + channel.writeScalar(pool.toXml()); + } catch (...) { + // Ignore write errors. + } + syscalls::fclose(f); } } catch (const boost::thread_interrupted &) { P_TRACE(2, "Status report thread interrupted."); @@ -114,6 +139,7 @@ class Server { } void deleteStatusReportFIFO() { + TRACE_POINT(); if (!statusReportFIFO.empty()) { int ret; do { @@ -121,6 +147,69 @@ class Server { } while (ret == -1 && errno == EINTR); } } + + /** + * Lowers this process's privilege to that of username, + * and sets stricter permissions for the Phusion Passenger temp + * directory. + */ + void lowerPrivilege(const string &username) { + struct passwd *entry; + int ret, e; + + entry = getpwnam(username.c_str()); + if (entry != NULL) { + do { + ret = chown(getPassengerTempDir().c_str(), + entry->pw_uid, entry->pw_gid); + } while (ret == -1 && errno == EINTR); + if (ret == -1) { + e = errno; + P_WARN("WARNING: Unable to change owner for directory '" << + getPassengerTempDir() << "' to '" << username << + "': " << strerror(e) << " (" << e << ")"); + } else { + do { + ret = chmod(getPassengerTempDir().c_str(), + S_IRUSR | S_IWUSR | S_IXUSR); + } while (ret == -1 && errno == EINTR); + if (ret == -1) { + e = errno; + P_WARN("WARNING: Unable to change " + "permissions for directory " << + getPassengerTempDir() << ": " << + strerror(e) << + " (" << e << ")"); + } + } + + if (initgroups(username.c_str(), entry->pw_gid) != 0) { + int e = errno; + P_WARN("WARNING: Unable to lower ApplicationPoolServerExecutable's " + "privilege to that of user '" << username << + "': cannot set supplementary groups for this " + "user: " << strerror(e) << " (" << e << ")"); + } + if (setgid(entry->pw_gid) != 0) { + int e = errno; + P_WARN("WARNING: Unable to lower ApplicationPoolServerExecutable's " + "privilege to that of user '" << username << + "': cannot set group ID: " << strerror(e) << + " (" << e << ")"); + } + if (setuid(entry->pw_uid) != 0) { + int e = errno; + P_WARN("WARNING: Unable to lower ApplicationPoolServerExecutable's " + "privilege to that of user '" << username << + "': cannot set user ID: " << strerror(e) << + " (" << e << ")"); + } + } else { + P_WARN("WARNING: Unable to lower ApplicationPoolServerExecutable's " + "privilege to that of user '" << username << + "': user does not exist."); + } + } public: Server(int serverSocket, @@ -135,21 +224,25 @@ class Server { Passenger::setLogLevel(logLevel); this->serverSocket = serverSocket; this->statusReportFIFO = statusReportFIFO; + this->user = user; } ~Server() { + TRACE_POINT(); this_thread::disable_syscall_interruption dsi; this_thread::disable_interruption di; P_TRACE(2, "Shutting down server."); - InterruptableCalls::close(serverSocket); + syscalls::close(serverSocket); if (statusReportThread != NULL) { - statusReportThread->interruptAndJoin(); + UPDATE_TRACE_POINT(); + statusReportThread->interrupt_and_join(); } // Wait for all clients to disconnect. + UPDATE_TRACE_POINT(); set clientsCopy; { /* If we clear _clients_ directly, then it may result in a deadlock. @@ -157,7 +250,7 @@ class Server { * the reference counts, and then we release all references outside the critical * section. */ - mutex::scoped_lock l(lock); + boost::mutex::scoped_lock l(lock); clientsCopy = clients; clients.clear(); } @@ -194,7 +287,7 @@ class Client { MessageChannel channel; /** The thread which handles the client connection. */ - Thread *thr; + oxt::thread *thr; /** * Maps session ID to sessions created by ApplicationPool::get(). Session IDs @@ -207,15 +300,17 @@ class Client { int lastSessionID; void processGet(const vector &args) { + TRACE_POINT(); Application::SessionPtr session; bool failed = false; try { - session = server.pool.get(args[1], args[2] == "true", args[3], - args[4], args[5], args[6]); + PoolOptions options(args, 1); + session = server.pool.get(options); sessions[lastSessionID] = session; lastSessionID++; } catch (const SpawnException &e) { + UPDATE_TRACE_POINT(); this_thread::disable_syscall_interruption dsi; if (e.hasErrorPage()) { @@ -230,22 +325,27 @@ class Client { } failed = true; } catch (const BusyException &e) { + UPDATE_TRACE_POINT(); this_thread::disable_syscall_interruption dsi; channel.write("BusyException", e.what(), NULL); failed = true; } catch (const IOException &e) { + UPDATE_TRACE_POINT(); this_thread::disable_syscall_interruption dsi; channel.write("IOException", e.what(), NULL); failed = true; } + UPDATE_TRACE_POINT(); if (!failed) { this_thread::disable_syscall_interruption dsi; try { + UPDATE_TRACE_POINT(); channel.write("ok", toString(session->getPid()).c_str(), toString(lastSessionID - 1).c_str(), NULL); channel.writeFileDescriptor(session->getStream()); session->closeStream(); } catch (const exception &) { + UPDATE_TRACE_POINT(); P_TRACE(3, "Client " << this << ": something went wrong " "while sending 'ok' back to the client."); sessions.erase(lastSessionID - 1); @@ -255,42 +355,47 @@ class Client { } void processClose(const vector &args) { + TRACE_POINT(); sessions.erase(atoi(args[1])); } void processClear(const vector &args) { + TRACE_POINT(); server.pool.clear(); } void processSetMaxIdleTime(const vector &args) { + TRACE_POINT(); server.pool.setMaxIdleTime(atoi(args[1])); } void processSetMax(const vector &args) { + TRACE_POINT(); server.pool.setMax(atoi(args[1])); } void processGetActive(const vector &args) { + TRACE_POINT(); channel.write(toString(server.pool.getActive()).c_str(), NULL); } void processGetCount(const vector &args) { + TRACE_POINT(); channel.write(toString(server.pool.getCount()).c_str(), NULL); } void processSetMaxPerApp(unsigned int maxPerApp) { + TRACE_POINT(); server.pool.setMaxPerApp(maxPerApp); } - void processSetUseGlobalQueue(bool value) { - server.pool.setUseGlobalQueue(value); - } - void processGetSpawnServerPid(const vector &args) { + TRACE_POINT(); channel.write(toString(server.pool.getSpawnServerPid()).c_str(), NULL); } void processUnknownMessage(const vector &args) { + TRACE_POINT(); string name; if (args.empty()) { name = "(null)"; @@ -305,9 +410,11 @@ class Client { * The entry point of the thread that handles the client connection. */ void threadMain(const weak_ptr self) { + TRACE_POINT(); vector args; try { while (!this_thread::interruption_requested()) { + UPDATE_TRACE_POINT(); try { if (!channel.read(args)) { // Client closed connection. @@ -322,7 +429,8 @@ class Client { P_TRACE(4, "Client " << this << ": received message: " << toString(args)); - if (args[0] == "get" && args.size() == 7) { + UPDATE_TRACE_POINT(); + if (args[0] == "get") { processGet(args); } else if (args[0] == "close" && args.size() == 2) { processClose(args); @@ -338,8 +446,6 @@ class Client { processGetCount(args); } else if (args[0] == "setMaxPerApp" && args.size() == 2) { processSetMaxPerApp(atoi(args[1])); - } else if (args[0] == "setUseGlobalQueue" && args.size() == 2) { - processSetUseGlobalQueue(args[1] == "true"); } else if (args[0] == "getSpawnServerPid" && args.size() == 1) { processGetSpawnServerPid(args); } else { @@ -350,13 +456,23 @@ class Client { } } catch (const boost::thread_interrupted &) { P_TRACE(2, "Client thread " << this << " interrupted."); + } catch (const tracable_exception &e) { + P_TRACE(2, "Uncaught exception in ApplicationPoolServer client thread:\n" + << " message: " << toString(args) << "\n" + << " exception: " << e.what() << "\n" + << " backtrace:\n" << e.backtrace()); } catch (const exception &e) { P_TRACE(2, "Uncaught exception in ApplicationPoolServer client thread:\n" << " message: " << toString(args) << "\n" - << " exception: " << e.what()); + << " exception: " << e.what() << "\n" + << " backtrace: not available"); + } catch (...) { + P_TRACE(2, "Uncaught unknown exception in ApplicationPool client thread."); + throw; } - mutex::scoped_lock l(server.lock); + UPDATE_TRACE_POINT(); + boost::mutex::scoped_lock l(server.lock); ClientPtr myself(self.lock()); if (myself != NULL) { server.clients.erase(myself); @@ -390,30 +506,34 @@ class Client { * connection. */ void start(const weak_ptr self) { - thr = new Thread( + stringstream name; + name << "Client " << fd; + thr = new oxt::thread( bind(&Client::threadMain, this, self), - CLIENT_THREAD_STACK_SIZE + name.str(), CLIENT_THREAD_STACK_SIZE ); } ~Client() { + TRACE_POINT(); this_thread::disable_syscall_interruption dsi; this_thread::disable_interruption di; if (thr != NULL) { if (thr->get_id() != this_thread::get_id()) { - thr->interruptAndJoin(); + thr->interrupt_and_join(); } delete thr; } - InterruptableCalls::close(fd); + syscalls::close(fd); } }; int Server::start() { - setupSyscallInterruptionSupport(); + TRACE_POINT(); + setup_syscall_interruption_support(); // Ignore SIGPIPE. struct sigaction action; @@ -425,20 +545,26 @@ Server::start() { try { if (!statusReportFIFO.empty()) { statusReportThread = ptr( - new Thread( + new oxt::thread( bind(&Server::statusReportThreadMain, this), + "Status report thread", 1024 * 128 ) ); } + if (!user.empty()) { + lowerPrivilege(user); + } + while (!this_thread::interruption_requested()) { int fds[2], ret; char x; // The received data only serves to wake up the server socket, // and is not important. - ret = InterruptableCalls::read(serverSocket, &x, 1); + UPDATE_TRACE_POINT(); + ret = syscalls::read(serverSocket, &x, 1); if (ret == 0) { // All web server processes disconnected from this server. // So we can safely quit. @@ -450,22 +576,28 @@ Server::start() { // We have an incoming connect request from an // ApplicationPool client. + UPDATE_TRACE_POINT(); do { ret = socketpair(AF_UNIX, SOCK_STREAM, 0, fds); } while (ret == -1 && errno == EINTR); if (ret == -1) { + UPDATE_TRACE_POINT(); throw SystemException("Cannot create an anonymous Unix socket", errno); } + UPDATE_TRACE_POINT(); MessageChannel(serverSocket).writeFileDescriptor(fds[1]); - InterruptableCalls::close(fds[1]); + syscalls::close(fds[1]); + UPDATE_TRACE_POINT(); ClientPtr client(new Client(*this, fds[0])); pair::iterator, bool> p; { - mutex::scoped_lock l(lock); + UPDATE_TRACE_POINT(); + boost::mutex::scoped_lock l(lock); clients.insert(client); } + UPDATE_TRACE_POINT(); client->start(client); } } catch (const boost::thread_interrupted &) { @@ -480,9 +612,15 @@ main(int argc, char *argv[]) { Server server(SERVER_SOCKET_FD, atoi(argv[1]), argv[2], argv[3], argv[4], argv[5], argv[6]); return server.start(); + } catch (const tracable_exception &e) { + P_ERROR(e.what() << "\n" << e.backtrace()); + return 1; } catch (const exception &e) { P_ERROR(e.what()); return 1; + } catch (...) { + P_ERROR("Unknown exception thrown in main thread."); + throw; } } diff --git a/ext/apache2/Bucket.cpp b/ext/apache2/Bucket.cpp index 7334eb59..4fe138fe 100644 --- a/ext/apache2/Bucket.cpp +++ b/ext/apache2/Bucket.cpp @@ -19,29 +19,67 @@ */ #include "Bucket.h" +using namespace Passenger; + +static void bucket_destroy(void *data); static apr_status_t bucket_read(apr_bucket *a, const char **str, apr_size_t *len, apr_read_type_e block); static const apr_bucket_type_t apr_bucket_type_passenger_pipe = { "PASSENGER_PIPE", 5, apr_bucket_type_t::APR_BUCKET_DATA, - apr_bucket_destroy_noop, + bucket_destroy, bucket_read, apr_bucket_setaside_notimpl, apr_bucket_split_notimpl, apr_bucket_copy_notimpl }; +struct BucketData { + Application::SessionPtr session; + apr_file_t *pipe; +}; + +static void +bucket_destroy(void *data) { + BucketData *bucket_data = (BucketData *) data; + if (data != NULL) { + delete bucket_data; + } +} + static apr_status_t bucket_read(apr_bucket *bucket, const char **str, apr_size_t *len, apr_read_type_e block) { apr_file_t *pipe; char *buf; apr_status_t ret; - pipe = (apr_file_t *) bucket->data; + BucketData *data = (BucketData *) bucket->data; + pipe = data->pipe; *str = NULL; *len = APR_BUCKET_BUFF_SIZE; + + if (block == APR_NONBLOCK_READ) { + /* + * The bucket brigade that Hooks::handleRequest() passes using + * ap_pass_brigade() is always passed through ap_content_length_filter, + * which is a filter which attempts to read all data from the + * bucket brigade and computes the Content-Length header from + * that. We don't want this to happen; because suppose that the + * Rails application sends back 1 GB of data, then + * ap_content_length_filter will buffer this entire 1 GB of data + * in memory before passing it to the HTTP client. + * + * ap_content_length_filter aborts and passes the bucket brigade + * down the filter chain when it encounters an APR_EAGAIN, except + * for the first read. So by returning APR_EAGAIN on every + * non-blocking read request, we can prevent ap_content_length_filter + * from buffering all data. + */ + return APR_EAGAIN; + } + buf = (char *) apr_bucket_alloc(*len, bucket->list); // TODO: check for failure? do { @@ -50,6 +88,8 @@ bucket_read(apr_bucket *bucket, const char **str, apr_size_t *len, apr_read_type if (ret != APR_SUCCESS && ret != APR_EOF) { // ... we might want to set an error flag here ... + delete data; + bucket->data = NULL; apr_bucket_free(buf); return ret; } @@ -59,13 +99,21 @@ bucket_read(apr_bucket *bucket, const char **str, apr_size_t *len, apr_read_type */ if (*len > 0) { apr_bucket_heap *h; + + *str = buf; + bucket->data = NULL; + /* Change the current bucket to refer to what we read */ bucket = apr_bucket_heap_make(bucket, buf, *len, apr_bucket_free); h = (apr_bucket_heap *) bucket->data; h->alloc_len = APR_BUCKET_BUFF_SIZE; /* note the real buffer size */ - *str = buf; - APR_BUCKET_INSERT_AFTER(bucket, passenger_bucket_create(pipe, bucket->list)); + APR_BUCKET_INSERT_AFTER(bucket, passenger_bucket_create( + data->session, pipe, bucket->list)); + delete data; } else { + delete data; + bucket->data = NULL; + apr_bucket_free(buf); bucket = apr_bucket_immortal_make(bucket, "", 0); *str = (const char *) bucket->data; @@ -76,23 +124,27 @@ bucket_read(apr_bucket *bucket, const char **str, apr_size_t *len, apr_read_type return APR_SUCCESS; } -apr_bucket * -passenger_bucket_make(apr_bucket *bucket, apr_file_t *pipe) { +static apr_bucket * +passenger_bucket_make(apr_bucket *bucket, Application::SessionPtr session, apr_file_t *pipe) { + BucketData *data = new BucketData(); + data->session = session; + data->pipe = pipe; + bucket->type = &apr_bucket_type_passenger_pipe; bucket->length = (apr_size_t)(-1); bucket->start = -1; - bucket->data = pipe; + bucket->data = data; return bucket; } apr_bucket * -passenger_bucket_create(apr_file_t *pipe, apr_bucket_alloc_t *list) { +passenger_bucket_create(Application::SessionPtr session, apr_file_t *pipe, apr_bucket_alloc_t *list) { apr_bucket *bucket; bucket = (apr_bucket *) apr_bucket_alloc(sizeof(*bucket), list); APR_BUCKET_INIT(bucket); bucket->free = apr_bucket_free; bucket->list = list; - return passenger_bucket_make(bucket, pipe); + return passenger_bucket_make(bucket, session, pipe); } diff --git a/ext/apache2/Bucket.h b/ext/apache2/Bucket.h index 80785e17..bc8e30f6 100644 --- a/ext/apache2/Bucket.h +++ b/ext/apache2/Bucket.h @@ -21,18 +21,25 @@ #define _PASSENGER_BUCKET_H_ /** - * apr_bucket_pipe closes a pipe's file descriptor when it has reached end-of-stream, - * but not when an error has occurred. That behavior conflicts with Phusion Passenger's - * file descriptor management code. + * apr_bucket_pipe closes a pipe's file descriptor when it has reached + * end-of-stream, but not when an error has occurred. This behavior is + * undesirable because it can easily cause file descriptor leaks. * - * passenger_bucket is like apr_bucket_pipe, but never closes the pipe's file descriptor. - * It also ignores the APR_NONBLOCK_READ because that's known to cause strange - * I/O problems. + * passenger_bucket is like apr_bucket_pipe, but it also holds a reference to + * a Session. When a read error has occured or when end-of-stream has been + * reached, the Session will be dereferenced, so that the underlying file + * descriptor is closed. + * + * passenger_bucket also ignores the APR_NONBLOCK_READ flag because that's + * known to cause strange I/O problems. */ -#include "apr_buckets.h" +#include +#include "Application.h" -apr_bucket *passenger_bucket_create(apr_file_t *pipe, apr_bucket_alloc_t *list); +apr_bucket *passenger_bucket_create(Passenger::Application::SessionPtr session, + apr_file_t *pipe, + apr_bucket_alloc_t *list); #endif /* _PASSENGER_BUCKET_H_ */ diff --git a/ext/apache2/CachedFileStat.cpp b/ext/apache2/CachedFileStat.cpp new file mode 100644 index 00000000..db140c09 --- /dev/null +++ b/ext/apache2/CachedFileStat.cpp @@ -0,0 +1,114 @@ +/* + * Phusion Passenger - http://www.modrails.com/ + * Copyright (C) 2009 Phusion + * + * Phusion Passenger is a trademark of Hongli Lai & Ninh Bui. + * + * 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. + */ +#include "CachedFileStat.h" + +#include +#include + +#include +#include + +using namespace std; +using namespace boost; +using namespace Passenger; + +// CachedMultiFileStat is written in C++, with a C wrapper API around it. +// I'm not going to reinvent my own linked list and hash table in C when I +// can just use the STL. +struct CachedMultiFileStat { + struct Item { + string filename; + CachedFileStat cstat; + + Item(const string &filename) + : cstat(filename) + { + this->filename = filename; + } + }; + + typedef shared_ptr ItemPtr; + typedef list ItemList; + typedef map ItemMap; + + unsigned int maxSize; + ItemList items; + ItemMap cache; + boost::mutex lock; + + CachedMultiFileStat(unsigned int maxSize) { + this->maxSize = maxSize; + } + + int stat(const string &filename, struct stat *buf, unsigned int throttleRate = 0) { + boost::unique_lock l(lock); + ItemMap::iterator it(cache.find(filename)); + ItemPtr item; + int ret; + + if (it == cache.end()) { + // Filename not in cache. + // If cache is full, remove the least recently used + // cache entry. + if (cache.size() == maxSize) { + ItemList::iterator listEnd(items.end()); + listEnd--; + string filename((*listEnd)->filename); + items.pop_back(); + cache.erase(filename); + } + + // Add to cache as most recently used. + item = ItemPtr(new Item(filename)); + items.push_front(item); + cache[filename] = items.begin(); + } else { + // Cache hit. + item = *it->second; + + // Mark this cache item as most recently used. + items.erase(it->second); + items.push_front(item); + cache[filename] = items.begin(); + } + ret = item->cstat.refresh(throttleRate); + *buf = item->cstat.info; + return ret; + } +}; + +CachedMultiFileStat * +cached_multi_file_stat_new(unsigned int max_size) { + return new CachedMultiFileStat(max_size); +} + +void +cached_multi_file_stat_free(CachedMultiFileStat *mstat) { + delete mstat; +} + +int +cached_multi_file_stat_perform(CachedMultiFileStat *mstat, + const char *filename, + struct stat *buf, + unsigned int throttle_rate) +{ + return mstat->stat(filename, buf, throttle_rate); +} diff --git a/ext/apache2/CachedFileStat.h b/ext/apache2/CachedFileStat.h new file mode 100644 index 00000000..930fc999 --- /dev/null +++ b/ext/apache2/CachedFileStat.h @@ -0,0 +1,169 @@ +/* + * Phusion Passenger - http://www.modrails.com/ + * Copyright (C) 2009 Phusion + * + * Phusion Passenger is a trademark of Hongli Lai & Ninh Bui. + * + * 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. + */ +#ifndef _PASSENGER_CACHED_FILE_STAT_H_ +#define _PASSENGER_CACHED_FILE_STAT_H_ + +#include +#include +#include +#include + +#ifdef __cplusplus + +#include +#include +#include + +#include "SystemTime.h" + +namespace Passenger { + +using namespace std; +using namespace oxt; + +/** + * CachedFileStat allows one to stat() a file at a throttled rate, in order + * to minimize stress on the filesystem. It does this by caching the old stat + * data for a specified amount of time. + */ +class CachedFileStat { +private: + /** The last return value of stat(). */ + int last_result; + + /** The errno set by the last stat() call. */ + int last_errno; + + /** The filename of the file to stat. */ + string filename; + + /** The last time a stat() was performed. */ + time_t last_time; + + /** + * Checks whether interval seconds have elapsed since begin + * The current time is returned via the currentTime argument, + * so that the caller doesn't have to call time() again if it needs the current + * time. + * + * @pre begin <= time(NULL) + * @return Whether interval seconds have elapsed since begin. + * @throws SystemException Something went wrong while retrieving the time. + * @throws boost::thread_interrupted + */ + bool expired(time_t begin, unsigned int interval, time_t ¤tTime) { + currentTime = SystemTime::get(); + return (unsigned int) (currentTime - begin) >= interval; + } + +public: + /** The cached stat info. */ + struct stat info; + + /** + * Creates a new CachedFileStat object. The file will not be + * stat()ted until you call refresh(). + * + * @param filename The file to stat. + */ + CachedFileStat(const string &filename) { + memset(&info, 0, sizeof(struct stat)); + last_result = -1; + last_errno = 0; + this->filename = filename; + last_time = 0; + } + + /** + * Re-stat() the file, if necessary. If throttleRate seconds have + * passed since the last time stat() was called, then the file will be + * re-stat()ted. + * + * The stat information, which may either be the result of a new stat() call + * or just the old cached information, is be available in the info + * member. + * + * @return 0 if the stat() call succeeded or if no stat() was performed, + * -1 if something went wrong while statting the file. In the latter + * case, errno will be populated with an appropriate error code. + * @throws SystemException Something went wrong while retrieving the system time. + * @throws boost::thread_interrupted + */ + int refresh(unsigned int throttleRate) { + time_t currentTime; + int ret; + + if (expired(last_time, throttleRate, currentTime)) { + ret = stat(filename.c_str(), &info); + if (ret == -1 && errno == EINTR) { + /* If the stat() call was interrupted, then don't + * update any state so that the caller can call + * this function again without us returning a + * cached EINTR error. + */ + return -1; + } else { + last_result = ret; + last_errno = errno; + last_time = currentTime; + return ret; + } + } else { + errno = last_errno; + return last_result; + } + } +}; + +} // namespace Passenger + +#endif /* __cplusplus */ + + +#ifdef __cplusplus + extern "C" { +#endif + +/** + * CachedMultiFileStat allows one to stat() files at a throttled rate, in order + * to minimize stress on the filesystem. It does this by caching the old stat + * data for a specified amount of time. + * + * Unlike CachedFileStat, which can only stat() one specific file per + * CachedFileStat object, CachedMultiFileStat can stat() any file. The + * number of cached stat() information is limited by the given cache size. + * + * This class is fully thread-safe. + */ +typedef struct CachedMultiFileStat CachedMultiFileStat; + +CachedMultiFileStat *cached_multi_file_stat_new(unsigned int max_size); +void cached_multi_file_stat_free(CachedMultiFileStat *mstat); +int cached_multi_file_stat_perform(CachedMultiFileStat *mstat, + const char *filename, + struct stat *buf, + unsigned int throttle_rate); + +#ifdef __cplusplus + } +#endif + +#endif /* _PASSENGER_CACHED_FILE_STAT_H_ */ + diff --git a/ext/apache2/Configuration.cpp b/ext/apache2/Configuration.cpp index 6ac4ebc4..ae08eb42 100644 --- a/ext/apache2/Configuration.cpp +++ b/ext/apache2/Configuration.cpp @@ -17,12 +17,16 @@ * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -#include #include #include #include "Configuration.h" #include "Utils.h" +/* The APR headers must come after the Passenger headers. See Hooks.cpp + * to learn why. + */ +#include + using namespace Passenger; extern "C" module AP_MODULE_DECLARE_DATA passenger_module; @@ -58,13 +62,27 @@ extern "C" { void * passenger_config_create_dir(apr_pool_t *p, char *dirspec) { DirConfig *config = create_dir_config_struct(p); + config->enabled = DirConfig::UNSET; config->autoDetectRails = DirConfig::UNSET; config->autoDetectRack = DirConfig::UNSET; config->autoDetectWSGI = DirConfig::UNSET; config->allowModRewrite = DirConfig::UNSET; config->railsEnv = NULL; + config->appRoot = NULL; config->rackEnv = NULL; config->spawnMethod = DirConfig::SM_UNSET; + config->frameworkSpawnerTimeout = -1; + config->appSpawnerTimeout = -1; + config->maxRequests = 0; + config->maxRequestsSpecified = false; + config->memoryLimit = 0; + config->memoryLimitSpecified = false; + config->highPerformance = DirConfig::UNSET; + config->useGlobalQueue = DirConfig::UNSET; + config->statThrottleRate = 0; + config->statThrottleRateSpecified = false; + config->restartDir = NULL; + /*************************************/ return config; } @@ -74,6 +92,8 @@ passenger_config_merge_dir(apr_pool_t *p, void *basev, void *addv) { DirConfig *base = (DirConfig *) basev; DirConfig *add = (DirConfig *) addv; + config->enabled = (add->enabled == DirConfig::UNSET) ? base->enabled : add->enabled; + config->railsBaseURIs = base->railsBaseURIs; for (set::const_iterator it(add->railsBaseURIs.begin()); it != add->railsBaseURIs.end(); it++) { config->railsBaseURIs.insert(*it); @@ -88,8 +108,21 @@ passenger_config_merge_dir(apr_pool_t *p, void *basev, void *addv) { config->autoDetectWSGI = (add->autoDetectWSGI == DirConfig::UNSET) ? base->autoDetectWSGI : add->autoDetectWSGI; config->allowModRewrite = (add->allowModRewrite == DirConfig::UNSET) ? base->allowModRewrite : add->allowModRewrite; config->railsEnv = (add->railsEnv == NULL) ? base->railsEnv : add->railsEnv; + config->appRoot = (add->appRoot == NULL) ? base->appRoot : add->appRoot; config->rackEnv = (add->rackEnv == NULL) ? base->rackEnv : add->rackEnv; config->spawnMethod = (add->spawnMethod == DirConfig::SM_UNSET) ? base->spawnMethod : add->spawnMethod; + config->frameworkSpawnerTimeout = (add->frameworkSpawnerTimeout == -1) ? base->frameworkSpawnerTimeout : add->frameworkSpawnerTimeout; + config->appSpawnerTimeout = (add->appSpawnerTimeout == -1) ? base->appSpawnerTimeout : add->appSpawnerTimeout; + config->maxRequests = (add->maxRequestsSpecified) ? add->maxRequests : base->maxRequests; + config->maxRequestsSpecified = base->maxRequestsSpecified || add->maxRequestsSpecified; + config->memoryLimit = (add->memoryLimitSpecified) ? add->memoryLimit : base->memoryLimit; + config->memoryLimitSpecified = base->memoryLimitSpecified || add->memoryLimitSpecified; + config->highPerformance = (add->highPerformance == DirConfig::UNSET) ? base->highPerformance : add->highPerformance; + config->useGlobalQueue = (add->useGlobalQueue == DirConfig::UNSET) ? base->useGlobalQueue : add->useGlobalQueue; + config->statThrottleRate = (add->statThrottleRateSpecified) ? add->statThrottleRate : base->statThrottleRate; + config->statThrottleRateSpecified = base->statThrottleRateSpecified || add->statThrottleRateSpecified; + config->restartDir = (add->restartDir == NULL) ? base->restartDir : add->restartDir; + /*************************************/ return config; } @@ -105,11 +138,10 @@ passenger_config_create_server(apr_pool_t *p, server_rec *s) { config->maxInstancesPerAppSpecified = false; config->poolIdleTime = DEFAULT_POOL_IDLE_TIME; config->poolIdleTimeSpecified = false; - config->useGlobalQueue = false; - config->useGlobalQueueSpecified = false; config->userSwitching = true; config->userSwitchingSpecified = false; config->defaultUser = NULL; + config->tempDir = NULL; return config; } @@ -128,11 +160,10 @@ passenger_config_merge_server(apr_pool_t *p, void *basev, void *addv) { config->maxInstancesPerAppSpecified = base->maxInstancesPerAppSpecified || add->maxInstancesPerAppSpecified; config->poolIdleTime = (add->poolIdleTime) ? base->poolIdleTime : add->poolIdleTime; config->poolIdleTimeSpecified = base->poolIdleTimeSpecified || add->poolIdleTimeSpecified; - config->useGlobalQueue = (add->useGlobalQueue) ? base->useGlobalQueue : add->useGlobalQueue; - config->useGlobalQueueSpecified = base->useGlobalQueueSpecified || add->useGlobalQueueSpecified; config->userSwitching = (add->userSwitchingSpecified) ? add->userSwitching : base->userSwitching; config->userSwitchingSpecified = base->userSwitchingSpecified || add->userSwitchingSpecified; config->defaultUser = (add->defaultUser == NULL) ? base->defaultUser : add->defaultUser; + config->tempDir = (add->tempDir == NULL) ? base->tempDir : add->tempDir; return config; } @@ -152,11 +183,10 @@ passenger_config_merge_all_servers(apr_pool_t *pool, server_rec *main_server) { final->maxInstancesPerAppSpecified = final->maxInstancesPerAppSpecified || config->maxInstancesPerAppSpecified; final->poolIdleTime = (final->poolIdleTimeSpecified) ? final->poolIdleTime : config->poolIdleTime; final->poolIdleTimeSpecified = final->poolIdleTimeSpecified || config->poolIdleTimeSpecified; - final->useGlobalQueue = (final->useGlobalQueue) ? final->useGlobalQueue : config->useGlobalQueue; - final->useGlobalQueueSpecified = final->useGlobalQueueSpecified || config->useGlobalQueueSpecified; final->userSwitching = (config->userSwitchingSpecified) ? config->userSwitching : final->userSwitching; final->userSwitchingSpecified = final->userSwitchingSpecified || config->userSwitchingSpecified; final->defaultUser = (final->defaultUser != NULL) ? final->defaultUser : config->defaultUser; + final->tempDir = (final->tempDir != NULL) ? final->tempDir : config->tempDir; } for (s = main_server; s != NULL; s = s->next) { ServerConfig *config = (ServerConfig *) ap_get_module_config(s->module_config, &passenger_module); @@ -249,8 +279,8 @@ cmd_passenger_pool_idle_time(cmd_parms *cmd, void *pcfg, const char *arg) { result = strtol(arg, &end, 10); if (*end != '\0') { return "Invalid number specified for PassengerPoolIdleTime."; - } else if (result <= 0) { - return "Value for PassengerPoolIdleTime must be greater than 0."; + } else if (result < 0) { + return "Value for PassengerPoolIdleTime must be greater than or equal to 0."; } else { config->poolIdleTime = (unsigned int) result; config->poolIdleTimeSpecified = true; @@ -260,14 +290,12 @@ cmd_passenger_pool_idle_time(cmd_parms *cmd, void *pcfg, const char *arg) { static const char * cmd_passenger_use_global_queue(cmd_parms *cmd, void *pcfg, int arg) { - ServerConfig *config = (ServerConfig *) ap_get_module_config( - cmd->server->module_config, &passenger_module); + DirConfig *config = (DirConfig *) pcfg; if (arg) { - config->useGlobalQueue = true; + config->useGlobalQueue = DirConfig::ENABLED; } else { - config->useGlobalQueue = false; + config->useGlobalQueue = DirConfig::DISABLED; } - config->useGlobalQueueSpecified = true; return NULL; } @@ -288,6 +316,86 @@ cmd_passenger_default_user(cmd_parms *cmd, void *dummy, const char *arg) { return NULL; } +static const char * +cmd_passenger_temp_dir(cmd_parms *cmd, void *dummy, const char *arg) { + ServerConfig *config = (ServerConfig *) ap_get_module_config( + cmd->server->module_config, &passenger_module); + config->tempDir = arg; + return NULL; +} + +static const char * +cmd_passenger_max_requests(cmd_parms *cmd, void *pcfg, const char *arg) { + DirConfig *config = (DirConfig *) pcfg; + char *end; + long int result; + + result = strtol(arg, &end, 10); + if (*end != '\0') { + return "Invalid number specified for PassengerMaxRequests."; + } else if (result < 0) { + return "Value for PassengerMaxRequests must be greater than or equal to 0."; + } else { + config->maxRequests = (unsigned long) result; + config->maxRequestsSpecified = true; + return NULL; + } +} + +static const char * +cmd_passenger_high_performance(cmd_parms *cmd, void *pcfg, int arg) { + DirConfig *config = (DirConfig *) pcfg; + if (arg) { + config->highPerformance = DirConfig::ENABLED; + } else { + config->highPerformance = DirConfig::DISABLED; + } + return NULL; +} + +static const char * +cmd_passenger_enabled(cmd_parms *cmd, void *pcfg, int arg) { + DirConfig *config = (DirConfig *) pcfg; + if (arg) { + config->enabled = DirConfig::ENABLED; + } else { + config->enabled = DirConfig::DISABLED; + } + return NULL; +} + +static const char * +cmd_passenger_stat_throttle_rate(cmd_parms *cmd, void *pcfg, const char *arg) { + DirConfig *config = (DirConfig *) pcfg; + char *end; + long int result; + + result = strtol(arg, &end, 10); + if (*end != '\0') { + return "Invalid number specified for PassengerStatThrottleRate."; + } else if (result < 0) { + return "Value for PassengerStatThrottleRate must be greater than or equal to 0."; + } else { + config->statThrottleRate = (unsigned long) result; + config->statThrottleRateSpecified = true; + return NULL; + } +} + +static const char * +cmd_passenger_restart_dir(cmd_parms *cmd, void *pcfg, const char *arg) { + DirConfig *config = (DirConfig *) pcfg; + config->restartDir = arg; + return NULL; +} + +static const char * +cmd_passenger_app_root(cmd_parms *cmd, void *pcfg, const char *arg) { + DirConfig *config = (DirConfig *) pcfg; + config->appRoot = arg; + return NULL; +} + /************************************************* * Rails-specific settings @@ -326,14 +434,50 @@ cmd_rails_spawn_method(cmd_parms *cmd, void *pcfg, const char *arg) { DirConfig *config = (DirConfig *) pcfg; if (strcmp(arg, "smart") == 0) { config->spawnMethod = DirConfig::SM_SMART; + } else if (strcmp(arg, "smart-lv2") == 0) { + config->spawnMethod = DirConfig::SM_SMART_LV2; } else if (strcmp(arg, "conservative") == 0) { config->spawnMethod = DirConfig::SM_CONSERVATIVE; } else { - return "RailsSpawnMethod may only be 'smart' or 'conservative'."; + return "RailsSpawnMethod may only be 'smart', 'smart-lv2' or 'conservative'."; } return NULL; } +static const char * +cmd_rails_framework_spawner_idle_time(cmd_parms *cmd, void *pcfg, const char *arg) { + DirConfig *config = (DirConfig *) pcfg; + char *end; + long int result; + + result = strtol(arg, &end, 10); + if (*end != '\0') { + return "Invalid number specified for RailsFrameworkSpawnerIdleTime."; + } else if (result < 0) { + return "Value for RailsFrameworkSpawnerIdleTime must be at least 0."; + } else { + config->frameworkSpawnerTimeout = result; + return NULL; + } +} + +static const char * +cmd_rails_app_spawner_idle_time(cmd_parms *cmd, void *pcfg, const char *arg) { + DirConfig *config = (DirConfig *) pcfg; + char *end; + long int result; + + result = strtol(arg, &end, 10); + if (*end != '\0') { + return "Invalid number specified for RailsAppSpawnerIdleTime."; + } else if (result < 0) { + return "Value for RailsAppSpawnerIdleTime must be at least 0."; + } else { + config->appSpawnerTimeout = result; + return NULL; + } +} + /************************************************* * Rack-specific settings @@ -387,7 +531,9 @@ cmd_rails_spawn_server(cmd_parms *cmd, void *pcfg, const char *arg) { } -typedef const char * (*Take1Func)(); // Workaround for some weird C++-specific compiler error. +// Workaround for some weird C++-specific compiler error. +typedef const char * (*Take0Func)(); +typedef const char * (*Take1Func)(); const command_rec passenger_commands[] = { // Passenger settings. @@ -424,7 +570,7 @@ const command_rec passenger_commands[] = { AP_INIT_FLAG("PassengerUseGlobalQueue", (Take1Func) cmd_passenger_use_global_queue, NULL, - ACCESS_CONF | RSRC_CONF, + OR_OPTIONS | ACCESS_CONF | RSRC_CONF, "Enable or disable Passenger's global queuing mode mode."), AP_INIT_FLAG("PassengerUserSwitching", (Take1Func) cmd_passenger_user_switching, @@ -436,12 +582,47 @@ const command_rec passenger_commands[] = { NULL, RSRC_CONF, "The user that Rails/Rack applications must run as when user switching fails or is disabled."), + AP_INIT_TAKE1("PassengerTempDir", + (Take1Func) cmd_passenger_temp_dir, + NULL, + RSRC_CONF, + "The temp directory that Passenger should use."), + AP_INIT_TAKE1("PassengerMaxRequests", + (Take1Func) cmd_passenger_max_requests, + NULL, + OR_LIMIT | ACCESS_CONF | RSRC_CONF, + "The maximum number of requests that an application instance may process."), + AP_INIT_FLAG("PassengerHighPerformance", + (Take1Func) cmd_passenger_high_performance, + NULL, + OR_ALL, + "Enable or disable Passenger's high performance mode."), + AP_INIT_FLAG("PassengerEnabled", + (Take1Func) cmd_passenger_enabled, + NULL, + OR_ALL, + "Enable or disable Phusion Passenger."), + AP_INIT_TAKE1("PassengerStatThrottleRate", + (Take1Func) cmd_passenger_stat_throttle_rate, + NULL, + OR_LIMIT | ACCESS_CONF | RSRC_CONF, + "Limit the number of stat calls to once per given seconds."), + AP_INIT_TAKE1("PassengerRestartDir", + (Take1Func) cmd_passenger_restart_dir, + NULL, + OR_OPTIONS | ACCESS_CONF | RSRC_CONF, + "The directory in which Passenger should look for restart.txt."), + AP_INIT_TAKE1("PassengerAppRoot", + (Take1Func) cmd_passenger_app_root, + NULL, + OR_OPTIONS | ACCESS_CONF | RSRC_CONF, + "The application's root directory."), // Rails-specific settings. AP_INIT_TAKE1("RailsBaseURI", (Take1Func) cmd_rails_base_uri, NULL, - RSRC_CONF, + OR_OPTIONS | ACCESS_CONF | RSRC_CONF, "Reserve the given URI to a Rails application."), AP_INIT_FLAG("RailsAutoDetect", (Take1Func) cmd_rails_auto_detect, @@ -456,19 +637,29 @@ const command_rec passenger_commands[] = { AP_INIT_TAKE1("RailsEnv", (Take1Func) cmd_rails_env, NULL, - RSRC_CONF, + OR_OPTIONS | ACCESS_CONF | RSRC_CONF, "The environment under which a Rails app must run."), AP_INIT_TAKE1("RailsSpawnMethod", (Take1Func) cmd_rails_spawn_method, NULL, RSRC_CONF, "The spawn method to use."), + AP_INIT_TAKE1("RailsFrameworkSpawnerIdleTime", // TODO: document this + (Take1Func) cmd_rails_framework_spawner_idle_time, + NULL, + RSRC_CONF, + "The maximum number of seconds that a framework spawner may be idle before it is shutdown."), + AP_INIT_TAKE1("RailsAppSpawnerIdleTime", // TODO: document this + (Take1Func) cmd_rails_app_spawner_idle_time, + NULL, + RSRC_CONF, + "The maximum number of seconds that an application spawner may be idle before it is shutdown."), // Rack-specific settings. AP_INIT_TAKE1("RackBaseURI", (Take1Func) cmd_rack_base_uri, NULL, - RSRC_CONF, + OR_OPTIONS | ACCESS_CONF | RSRC_CONF, "Reserve the given URI to a Rack application."), AP_INIT_FLAG("RackAutoDetect", (Take1Func) cmd_rack_auto_detect, @@ -478,10 +669,10 @@ const command_rec passenger_commands[] = { AP_INIT_TAKE1("RackEnv", (Take1Func) cmd_rack_env, NULL, - RSRC_CONF, + OR_OPTIONS | ACCESS_CONF | RSRC_CONF, "The environment under which a Rack app must run."), - // Rack-specific settings. + // WSGI-specific settings. AP_INIT_FLAG("PassengerWSGIAutoDetect", (Take1Func) cmd_wsgi_auto_detect, NULL, diff --git a/ext/apache2/Configuration.h b/ext/apache2/Configuration.h index 52e44908..c4055637 100644 --- a/ext/apache2/Configuration.h +++ b/ext/apache2/Configuration.h @@ -20,6 +20,16 @@ #ifndef _PASSENGER_CONFIGURATION_H_ #define _PASSENGER_CONFIGURATION_H_ +#include "Utils.h" +#include "MessageChannel.h" + +/* The APR headers must come after the Passenger headers. See Hooks.cpp + * to learn why. + * + * MessageChannel.h must be included -- even though we don't actually use + * MessageChannel.h in here, it's necessary to make sure that apr_want.h + * doesn't b0rk on 'struct iovec'. + */ #include #include #include @@ -31,7 +41,7 @@ */ /** Module version number. */ -#define PASSENGER_VERSION "2.0.6" +#define PASSENGER_VERSION "2.1.1" #ifdef __cplusplus #include @@ -43,9 +53,15 @@ /** * Per-directory configuration information. + * + * Use the getter methods to query information, because those will return + * the default value if the value is not specified. */ struct DirConfig { enum Threeway { ENABLED, DISABLED, UNSET }; + enum SpawnMethod { SM_UNSET, SM_SMART, SM_SMART_LV2, SM_CONSERVATIVE }; + + Threeway enabled; std::set railsBaseURIs; std::set rackBaseURIs; @@ -66,17 +82,166 @@ * Rails applications should operate. */ const char *railsEnv; + /** The path to the application's root (for example: RAILS_ROOT + * for Rails applications, directory containing 'config.ru' + * for Rack applications). If this value is NULL, the default + * autodetected path will be used. + */ + const char *appRoot; + /** The environment (i.e. value for RACK_ENV) under which * Rack applications should operate. */ const char *rackEnv; - enum SpawnMethod { SM_UNSET, SM_SMART, SM_CONSERVATIVE }; /** The Rails spawn method to use. */ SpawnMethod spawnMethod; + + /** + * The idle timeout, in seconds, of Rails framework spawners. + * May also be 0 (which indicates that the framework spawner should + * never idle timeout) or -1 (which means that the value is not specified). + */ + long frameworkSpawnerTimeout; + + /** + * The idle timeout, in seconds, of Rails application spawners. + * May also be 0 (which indicates that the application spawner should + * never idle timeout) or -1 (which means that the value is not specified). + */ + long appSpawnerTimeout; + + /** + * The maximum number of requests that the spawned application may process + * before exiting. A value of 0 means unlimited. + */ + unsigned long maxRequests; + + /** Indicates whether the maxRequests option was explicitly specified + * in the directory configuration. */ + bool maxRequestsSpecified; + + /** + * The maximum amount of memory (in MB) the spawned application may use. + * A value of 0 means unlimited. + */ + unsigned long memoryLimit; + + /** Indicates whether the memoryLimit option was explicitly specified + * in the directory configuration. */ + bool memoryLimitSpecified; + + Threeway highPerformance; + + /** Whether global queuing should be used. */ + Threeway useGlobalQueue; + + /** + * Throttle the number of stat() calls on files like + * restart.txt to the once per given number of seconds. + */ + unsigned long statThrottleRate; + + /** Indicates whether the statThrottleRate option was + * explicitly specified in the directory configuration. */ + bool statThrottleRateSpecified; + + /** The directory in which Passenger should look for + * restart.txt. NULL means that the default directory + * should be used. + */ + const char *restartDir; + + /*************************************/ + + bool isEnabled() const { + return enabled != DISABLED; + } + + string getAppRoot(const char *documentRoot) const { + if (appRoot == NULL) { + return string(documentRoot).append("/.."); + } else { + return appRoot; + } + } + + const char *getRailsEnv() const { + if (railsEnv != NULL) { + return railsEnv; + } else { + return "production"; + } + } + + const char *getRackEnv() const { + if (rackEnv != NULL) { + return rackEnv; + } else { + return "production"; + } + } + + const char *getSpawnMethodString() { + switch (spawnMethod) { + case SM_SMART: + return "smart"; + case SM_SMART_LV2: + return "smart-lv2"; + case SM_CONSERVATIVE: + return "conservative"; + default: + return "smart-lv2"; + } + } + + unsigned long getMaxRequests() { + if (maxRequestsSpecified) { + return maxRequests; + } else { + return 0; + } + } + + unsigned long getMemoryLimit() { + if (memoryLimitSpecified) { + return memoryLimit; + } else { + return 200; + } + } + + bool highPerformanceMode() const { + return highPerformance == ENABLED; + } + + bool usingGlobalQueue() const { + return useGlobalQueue == ENABLED; + } + + unsigned long getStatThrottleRate() const { + if (statThrottleRateSpecified) { + return statThrottleRate; + } else { + return 0; + } + } + + const char *getRestartDir() const { + if (restartDir != NULL) { + return restartDir; + } else { + return ""; + } + } + + /*************************************/ }; /** * Server-wide (global, not per-virtual host) configuration information. + * + * Use the getter methods to query information, because those will return + * the default value if the value is not specified. */ struct ServerConfig { /** The filename of the Ruby interpreter to use. */ @@ -112,13 +277,6 @@ * this server config. */ bool poolIdleTimeSpecified; - /** Whether global queuing should be used. */ - bool useGlobalQueue; - - /** Whether the useGlobalQueue option was explicitly specified - * in this server config. */ - bool useGlobalQueueSpecified; - /** Whether user switching support is enabled. */ bool userSwitching; @@ -131,11 +289,16 @@ */ const char *defaultUser; - bool getUseGlobalQueue() const { - if (useGlobalQueueSpecified) { - return useGlobalQueue; + /** The temp directory that Passenger should use. NULL + * means unspecified. + */ + const char *tempDir; + + const char *getDefaultUser() const { + if (defaultUser != NULL) { + return defaultUser; } else { - return false; + return "nobody"; } } }; diff --git a/ext/apache2/DirectoryMapper.h b/ext/apache2/DirectoryMapper.h new file mode 100644 index 00000000..e10aab24 --- /dev/null +++ b/ext/apache2/DirectoryMapper.h @@ -0,0 +1,287 @@ +/* + * Phusion Passenger - http://www.modrails.com/ + * Copyright (C) 2008 Phusion + * + * Phusion Passenger is a trademark of Hongli Lai & Ninh Bui. + * + * 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. + */ +#ifndef _PASSENGER_DIRECTORY_MAPPER_H_ +#define _PASSENGER_DIRECTORY_MAPPER_H_ + +#include +#include +#include + +#include + +#include "CachedFileStat.h" +#include "Configuration.h" +#include "Utils.h" + +// The Apache/APR headers *must* come after the Boost headers, otherwise +// compilation will fail on OpenBSD. +#include +#include + +namespace Passenger { + +using namespace std; +using namespace oxt; + +/** + * Utility class for determining URI-to-application directory mappings. + * Given a URI, it will determine whether that URI belongs to a Phusion + * Passenger-handled application, what the base URI of that application is, + * and what the associated 'public' directory is. + * + * @note This class is not thread-safe, but is reentrant. + * @ingroup Core + */ +class DirectoryMapper { +public: + enum ApplicationType { + NONE, + RAILS, + RACK, + WSGI + }; + +private: + DirConfig *config; + request_rec *r; + CachedMultiFileStat *mstat; + unsigned int throttleRate; + bool baseURIKnown; + const char *baseURI; + ApplicationType appType; + + inline bool shouldAutoDetectRails() { + return config->autoDetectRails == DirConfig::ENABLED || + config->autoDetectRails == DirConfig::UNSET; + } + + inline bool shouldAutoDetectRack() { + return config->autoDetectRack == DirConfig::ENABLED || + config->autoDetectRack == DirConfig::UNSET; + } + + inline bool shouldAutoDetectWSGI() { + return config->autoDetectWSGI == DirConfig::ENABLED || + config->autoDetectWSGI == DirConfig::UNSET; + } + +public: + /** + * Create a new DirectoryMapper object. + * + * @param mstat A CachedMultiFileStat object used for statting files. + * @param throttleRate A throttling rate for mstat. + * @warning Do not use this object after the destruction of r, + * config or mstat. + */ + DirectoryMapper(request_rec *r, DirConfig *config, + CachedMultiFileStat *mstat, unsigned int throttleRate) { + this->r = r; + this->config = config; + this->mstat = mstat; + this->throttleRate = throttleRate; + appType = NONE; + baseURIKnown = false; + baseURI = NULL; + } + + /** + * Determine whether the given HTTP request falls under one of the specified + * RailsBaseURIs or RackBaseURIs. If yes, then the first matching base URI will + * be returned. + * + * If Rails/Rack autodetection was enabled in the configuration, and the document + * root seems to be a valid Rails/Rack 'public' folder, then this method will + * return "/". + * + * Otherwise, NULL will be returned. + * + * @throws FileSystemException This method might also examine the filesystem in + * order to detect the application's type. During that process, a + * FileSystemException might be thrown. + * @warning The return value may only be used as long as config + * hasn't been destroyed. + */ + const char *getBaseURI() { + TRACE_POINT(); + if (baseURIKnown) { + return baseURI; + } + + set::const_iterator it; + const char *uri = r->uri; + size_t uri_len = strlen(uri); + + if (uri_len == 0 || uri[0] != '/') { + baseURIKnown = true; + return NULL; + } + + UPDATE_TRACE_POINT(); + for (it = config->railsBaseURIs.begin(); it != config->railsBaseURIs.end(); it++) { + const string &base(*it); + if ( base == "/" + || ( uri_len == base.size() && memcmp(uri, base.c_str(), uri_len) == 0 ) + || ( uri_len > base.size() && memcmp(uri, base.c_str(), base.size()) == 0 + && uri[base.size()] == '/' ) + ) { + baseURIKnown = true; + baseURI = base.c_str(); + appType = RAILS; + return baseURI; + } + } + + UPDATE_TRACE_POINT(); + for (it = config->rackBaseURIs.begin(); it != config->rackBaseURIs.end(); it++) { + const string &base(*it); + if ( base == "/" + || ( uri_len == base.size() && memcmp(uri, base.c_str(), uri_len) == 0 ) + || ( uri_len > base.size() && memcmp(uri, base.c_str(), base.size()) == 0 + && uri[base.size()] == '/' ) + ) { + baseURIKnown = true; + baseURI = base.c_str(); + appType = RACK; + return baseURI; + } + } + + UPDATE_TRACE_POINT(); + if (shouldAutoDetectRails() + && verifyRailsDir(config->getAppRoot(ap_document_root(r)), mstat, throttleRate)) { + baseURIKnown = true; + baseURI = "/"; + appType = RAILS; + return baseURI; + } + + UPDATE_TRACE_POINT(); + if (shouldAutoDetectRack() + && verifyRackDir(config->getAppRoot(ap_document_root(r)), mstat, throttleRate)) { + baseURIKnown = true; + baseURI = "/"; + appType = RACK; + return baseURI; + } + + UPDATE_TRACE_POINT(); + if (shouldAutoDetectWSGI() + && verifyWSGIDir(config->getAppRoot(ap_document_root(r)), mstat, throttleRate)) { + baseURIKnown = true; + baseURI = "/"; + appType = WSGI; + return baseURI; + } + + baseURIKnown = true; + return NULL; + } + + /** + * Returns the filename of the 'public' directory of the Rails/Rack application + * that's associated with the HTTP request. + * + * Returns an empty string if the document root of the HTTP request + * cannot be determined, or if it isn't a valid folder. + * + * @throws FileSystemException An error occured while examening the filesystem. + */ + string getPublicDirectory() { + if (!baseURIKnown) { + getBaseURI(); + } + if (baseURI == NULL) { + return ""; + } + + const char *docRoot = ap_document_root(r); + size_t len = strlen(docRoot); + if (len > 0) { + string path; + if (docRoot[len - 1] == '/') { + path.assign(docRoot, len - 1); + } else { + path.assign(docRoot, len); + } + if (strcmp(baseURI, "/") != 0) { + path.append(baseURI); + } + return path; + } else { + return ""; + } + } + + /** + * Returns the application type that's associated with the HTTP request. + * + * @throws FileSystemException An error occured while examening the filesystem. + */ + ApplicationType getApplicationType() { + if (!baseURIKnown) { + getBaseURI(); + } + return appType; + } + + /** + * Returns the application type (as a string) that's associated + * with the HTTP request. + * + * @throws FileSystemException An error occured while examening the filesystem. + */ + const char *getApplicationTypeString() { + if (!baseURIKnown) { + getBaseURI(); + } + switch (appType) { + case RAILS: + return "rails"; + case RACK: + return "rack"; + case WSGI: + return "wsgi"; + default: + return NULL; + }; + } + + /** + * Returns the environment under which the application should be spawned. + * + * @throws FileSystemException An error occured while examening the filesystem. + */ + const char *getEnvironment() { + switch (getApplicationType()) { + case RAILS: + return config->getRailsEnv(); + case RACK: + return config->getRackEnv(); + default: + return "production"; + } + } +}; + +} // namespace Passenger + +#endif /* _PASSENGER_DIRECTORY_MAPPER_H_ */ + diff --git a/ext/apache2/Exceptions.h b/ext/apache2/Exceptions.h index fc55f52b..4018efc5 100644 --- a/ext/apache2/Exceptions.h +++ b/ext/apache2/Exceptions.h @@ -20,7 +20,7 @@ #ifndef _PASSENGER_EXCEPTIONS_H_ #define _PASSENGER_EXCEPTIONS_H_ -#include +#include #include #include #include @@ -41,7 +41,7 @@ using namespace std; * * @ingroup Exceptions */ -class SystemException: public exception { +class SystemException: public oxt::tracable_exception { private: string briefMessage; string systemMessage; @@ -51,23 +51,22 @@ class SystemException: public exception { /** * Create a new SystemException. * - * @param message A message describing the error. + * @param briefMessage A brief message describing the error. * @param errorCode The error code, i.e. the value of errno right after the error occured. * @note A system description of the error will be appended to the given message. - * For example, if errorCode is EBADF, and message + * For example, if errorCode is EBADF, and briefMessage * is "Something happened", then what() will return "Something happened: Bad * file descriptor (10)" (if 10 is the number for EBADF). * @post code() == errorCode - * @post brief() == message + * @post brief() == briefMessage */ - SystemException(const string &message, int errorCode) { + SystemException(const string &briefMessage, int errorCode) { stringstream str; - briefMessage = message; str << strerror(errorCode) << " (" << errorCode << ")"; systemMessage = str.str(); - fullMessage = briefMessage + ": " + systemMessage; + setBriefMessage(briefMessage); m_code = errorCode; } @@ -77,6 +76,11 @@ class SystemException: public exception { return fullMessage.c_str(); } + void setBriefMessage(const string &message) { + briefMessage = message; + fullMessage = briefMessage + ": " + systemMessage; + } + /** * The value of errno at the time the error occured. */ @@ -132,7 +136,7 @@ class FileSystemException: public SystemException { * * @ingroup Exceptions */ -class IOException: public exception { +class IOException: public oxt::tracable_exception { private: string msg; public: @@ -153,7 +157,7 @@ class FileNotFoundException: public IOException { /** * Thrown when an invalid configuration is given. */ -class ConfigurationException: public exception { +class ConfigurationException: public oxt::tracable_exception { private: string msg; public: @@ -167,7 +171,7 @@ class ConfigurationException: public exception { * instance. The exception may contain an error page, which is a user-friendly * HTML page with details about the error. */ -class SpawnException: public exception { +class SpawnException: public oxt::tracable_exception { private: string msg; bool m_hasErrorPage; @@ -203,12 +207,26 @@ class SpawnException: public exception { } }; +/** + * A generic runtime exception. + * + * @ingroup Exceptions + */ +class RuntimeException: public oxt::tracable_exception { +private: + string msg; +public: + RuntimeException(const string &message): msg(message) {} + virtual ~RuntimeException() throw() {} + virtual const char *what() const throw() { return msg.c_str(); } +}; + /** * The application pool is too busy and cannot fulfill a get() request. * * @ingroup Exceptions */ -class BusyException: public exception { +class BusyException: public oxt::tracable_exception { private: string msg; public: diff --git a/ext/apache2/FileChecker.h b/ext/apache2/FileChecker.h new file mode 100644 index 00000000..39b94bb7 --- /dev/null +++ b/ext/apache2/FileChecker.h @@ -0,0 +1,108 @@ +/* + * Phusion Passenger - http://www.modrails.com/ + * Copyright (C) 2009 Phusion + * + * Phusion Passenger is a trademark of Hongli Lai & Ninh Bui. + * + * 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. + */ +#ifndef _PASSENGER_FILE_CHECKER_H_ +#define _PASSENGER_FILE_CHECKER_H_ + +#include + +#include + +#include +#include + +#include "CachedFileStat.h" +#include "SystemTime.h" + +namespace Passenger { + +using namespace std; +using namespace oxt; + +/** + * Utility class for checking for file changes. Example: + * + * @code + * FileChecker checker("foo.txt"); + * checker.changed(); // false + * writeToFile("foo.txt"); + * checker.changed(); // true + * checker.changed(); // false + * @endcode + * + * FileChecker uses stat() to retrieve file information. FileChecker also + * supports throttling in order to limit the number of stat() calls. This + * can improve performance on systems where disk I/O is a problem. + */ +class FileChecker { +private: + CachedFileStat cstat; + time_t lastMtime; + time_t lastCtime; + +public: + /** + * Create a FileChecker object. + * + * @param filename The filename to check for. + */ + FileChecker(const string &filename) + : cstat(filename) + { + lastMtime = 0; + lastCtime = 0; + changed(); + } + + /** + * Checks whether the file's timestamp has changed or has been created + * or removed since the last call to changed(). + * + * @param throttleRate When set to a non-zero value, throttling will be + * enabled. stat() will be called at most once per + * throttleRate seconds. + * @throws SystemException Something went wrong. + * @throws boost::thread_interrupted + */ + bool changed(unsigned int throttleRate = 0) { + int ret; + time_t ctime, mtime; + bool result; + + do { + ret = cstat.refresh(throttleRate); + } while (ret == -1 && errno == EINTR); + + if (ret == -1) { + ctime = 0; + mtime = 0; + } else { + ctime = cstat.info.st_ctime; + mtime = cstat.info.st_mtime; + } + result = lastMtime != mtime || lastCtime != ctime; + lastMtime = mtime; + lastCtime = ctime; + return result; + } +}; + +} // namespace Passenger + +#endif /* _PASSENGER_FILE_CHECKER_H_ */ diff --git a/ext/apache2/Hooks.cpp b/ext/apache2/Hooks.cpp index 3fee56d4..659d8bf0 100644 --- a/ext/apache2/Hooks.cpp +++ b/ext/apache2/Hooks.cpp @@ -17,18 +17,6 @@ * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - #include #include @@ -44,17 +32,34 @@ #include "Logging.h" #include "ApplicationPoolServer.h" #include "MessageChannel.h" -#include "System.h" +#include "DirectoryMapper.h" + +/* The Apache/APR headers *must* come after the Boost headers, otherwise + * compilation will fail on OpenBSD. + * + * apr_want.h *must* come after MessageChannel.h, otherwise compilation will + * fail on platforms on which apr_want.h tries to redefine 'struct iovec'. + * http://tinyurl.com/b6aatw + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include using namespace std; using namespace Passenger; extern "C" module AP_MODULE_DECLARE_DATA passenger_module; + #define DEFAULT_RUBY_COMMAND "ruby" -#define DEFAULT_RAILS_ENV "production" -#define DEFAULT_RACK_ENV "production" -#define DEFAULT_WSGI_ENV "production" /** * If the HTTP client sends POST data larger than this value (in bytes), @@ -65,261 +70,150 @@ extern "C" module AP_MODULE_DECLARE_DATA passenger_module; /** - * Utility class for determining URI-to-Rails/Rack directory mappings. - * Given a URI, it will determine whether that URI belongs to a Rails/Rack - * application, what the base URI of that application is, and what the - * associated 'public' directory is. + * Apache hook functions, wrapped in a class. * - * @note This class is not thread-safe, but is reentrant. * @ingroup Core */ -class DirectoryMapper { -public: - enum ApplicationType { - NONE, - RAILS, - RACK, - WSGI - }; - +class Hooks { private: - DirConfig *config; - request_rec *r; - bool baseURIKnown; - const char *baseURI; - ApplicationType appType; - - inline bool shouldAutoDetectRails() { - return config->autoDetectRails == DirConfig::ENABLED || - config->autoDetectRails == DirConfig::UNSET; - } - - inline bool shouldAutoDetectRack() { - return config->autoDetectRack == DirConfig::ENABLED || - config->autoDetectRack == DirConfig::UNSET; - } - - inline bool shouldAutoDetectWSGI() { - return config->autoDetectWSGI == DirConfig::ENABLED || - config->autoDetectWSGI == DirConfig::UNSET; - } - -public: - /** - * @warning Do not use this object after the destruction of r or config. - */ - DirectoryMapper(request_rec *r, DirConfig *config) { - this->r = r; - this->config = config; - appType = NONE; - baseURIKnown = false; - baseURI = NULL; - } - - /** - * Determine whether the given HTTP request falls under one of the specified - * RailsBaseURIs or RackBaseURIs. If yes, then the first matching base URI will - * be returned. - * - * If Rails/Rack autodetection was enabled in the configuration, and the document - * root seems to be a valid Rails/Rack 'public' folder, then this method will - * return "/". - * - * Otherwise, NULL will be returned. - * - * @throws SystemException An error occured while examening the filesystem. - * @warning The return value may only be used as long as config - * hasn't been destroyed. - */ - const char *getBaseURI() { - if (baseURIKnown) { - return baseURI; - } - - set::const_iterator it; - const char *uri = r->uri; - size_t uri_len = strlen(uri); - - if (uri_len == 0 || uri[0] != '/') { - baseURIKnown = true; - return NULL; - } - - for (it = config->railsBaseURIs.begin(); it != config->railsBaseURIs.end(); it++) { - const string &base(*it); - if ( base == "/" - || ( uri_len == base.size() && memcmp(uri, base.c_str(), uri_len) == 0 ) - || ( uri_len > base.size() && memcmp(uri, base.c_str(), base.size()) == 0 - && uri[base.size()] == '/' ) - ) { - baseURIKnown = true; - baseURI = base.c_str(); - appType = RAILS; - return baseURI; - } - } - - for (it = config->rackBaseURIs.begin(); it != config->rackBaseURIs.end(); it++) { - const string &base(*it); - if ( base == "/" - || ( uri_len == base.size() && memcmp(uri, base.c_str(), uri_len) == 0 ) - || ( uri_len > base.size() && memcmp(uri, base.c_str(), base.size()) == 0 - && uri[base.size()] == '/' ) - ) { - baseURIKnown = true; - baseURI = base.c_str(); - appType = RACK; - return baseURI; - } - } + struct AprDestructable { + virtual ~AprDestructable() { } - if (shouldAutoDetectRails() && verifyRailsDir(ap_document_root(r))) { - baseURIKnown = true; - baseURI = "/"; - appType = RAILS; - return baseURI; - } - if (shouldAutoDetectRack() && verifyRackDir(ap_document_root(r))) { - baseURIKnown = true; - baseURI = "/"; - appType = RACK; - return baseURI; - } - if (shouldAutoDetectWSGI() && verifyWSGIDir(ap_document_root(r))) { - baseURIKnown = true; - baseURI = "/"; - appType = WSGI; - return baseURI; + static apr_status_t cleanup(void *p) { + delete (AprDestructable *) p; + return APR_SUCCESS; } - - baseURIKnown = true; - return NULL; - } + }; - /** - * Returns the filename of the 'public' directory of the Rails/Rack application - * that's associated with the HTTP request. - * - * Returns an empty string if the document root of the HTTP request - * cannot be determined, or if it isn't a valid folder. - * - * @throws SystemException An error occured while examening the filesystem. - */ - string getPublicDirectory() { - if (!baseURIKnown) { - getBaseURI(); - } - if (baseURI == NULL) { - return ""; - } + struct RequestNote: public AprDestructable { + DirectoryMapper mapper; + DirConfig *config; + bool forwardToBackend; + const char *handlerBeforeModRewrite; + char *filenameBeforeModRewrite; + apr_filetype_e oldFileType; + const char *handlerBeforeModAutoIndex; - const char *docRoot = ap_document_root(r); - size_t len = strlen(docRoot); - if (len > 0) { - string path; - if (docRoot[len - 1] == '/') { - path.assign(docRoot, len - 1); - } else { - path.assign(docRoot, len); - } - if (strcmp(baseURI, "/") != 0) { - path.append(baseURI); - } - return path; - } else { - return ""; + RequestNote(const DirectoryMapper &m) + : mapper(m) { + forwardToBackend = false; + filenameBeforeModRewrite = NULL; } - } + }; - /** - * Returns the application type that's associated with the HTTP request. - * - * @throws SystemException An error occured while examening the filesystem. - */ - ApplicationType getApplicationType() { - if (!baseURIKnown) { - getBaseURI(); - } - return appType; - } + struct ErrorReport: public AprDestructable { + virtual int report(request_rec *r) = 0; + }; - /** - * Returns the application type (as a string) that's associated - * with the HTTP request. - * - * @throws SystemException An error occured while examening the filesystem. - */ - const char *getApplicationTypeString() { - if (!baseURIKnown) { - getBaseURI(); - } - switch (appType) { - case RAILS: - return "rails"; - case RACK: - return "rack"; - case WSGI: - return "wsgi"; - default: - return NULL; - }; - } -}; - - -/** - * Apache hook functions, wrapped in a class. - * - * @ingroup Core - */ -class Hooks { -private: - struct Container { - Application::SessionPtr session; + struct ReportFileSystemError: public ErrorReport { + FileSystemException e; - static apr_status_t cleanup(void *p) { - try { - this_thread::disable_interruption di; - this_thread::disable_syscall_interruption dsi; - delete (Container *) p; - } catch (const thread_interrupted &) { - P_TRACE(3, "A system call was interrupted during closing " - "of a session. Apache is probably restarting or " - "shutting down."); - } catch (const exception &e) { - P_TRACE(3, "Exception during closing of a session: " << - e.what()); + ReportFileSystemError(const FileSystemException &ex): e(ex) { } + + int report(request_rec *r) { + ap_set_content_type(r, "text/html; charset=UTF-8"); + ap_rputs("

Passenger error #2

\n", r); + ap_rputs("An error occurred while trying to access '", r); + ap_rputs(ap_escape_html(r->pool, e.filename().c_str()), r); + ap_rputs("': ", r); + ap_rputs(ap_escape_html(r->pool, e.what()), r); + if (e.code() == EPERM) { + ap_rputs("

", r); + ap_rputs("Apache doesn't have read permissions to that file. ", r); + ap_rputs("Please fix the relevant file permissions.", r); + ap_rputs("

", r); } - return APR_SUCCESS; + P_ERROR("A filesystem exception occured.\n" << + " Message: " << e.what() << "\n" << + " Backtrace:\n" << e.backtrace()); + return OK; } }; + + enum Threeway { YES, NO, UNKNOWN }; ApplicationPoolServerPtr applicationPoolServer; thread_specific_ptr threadSpecificApplicationPool; + Threeway m_hasModRewrite, m_hasModDir, m_hasModAutoIndex; + CachedMultiFileStat *mstat; - DirConfig *getDirConfig(request_rec *r) { + inline DirConfig *getDirConfig(request_rec *r) { return (DirConfig *) ap_get_module_config(r->per_dir_config, &passenger_module); } - ServerConfig *getServerConfig(server_rec *s) { + inline ServerConfig *getServerConfig(server_rec *s) { return (ServerConfig *) ap_get_module_config(s->module_config, &passenger_module); } + inline RequestNote *getRequestNote(request_rec *r) { + // The union is needed in order to be compliant with + // C99/C++'s strict aliasing rules. http://tinyurl.com/g5hgh + union { + RequestNote *note; + void *pointer; + } u; + u.note = 0; + apr_pool_userdata_get(&u.pointer, "Phusion Passenger", r->pool); + return u.note; + } + /** + * Returns a usable ApplicationPool object. + * * When using the worker MPM and global queuing, deadlocks can occur, for * the same reason described in ApplicationPoolServer::connect(). This * method allows us to avoid this deadlock by making sure that each * thread gets its own connection to the application pool server. + * + * It also checks whether the currently cached ApplicationPool object + * is disconnected (which can happen if an error previously occured). + * If so, it will reconnect to the ApplicationPool server. */ ApplicationPoolPtr getApplicationPool() { ApplicationPoolPtr *pool_ptr = threadSpecificApplicationPool.get(); if (pool_ptr == NULL) { pool_ptr = new ApplicationPoolPtr(applicationPoolServer->connect()); threadSpecificApplicationPool.reset(pool_ptr); + } else if (!(*pool_ptr)->connected()) { + P_DEBUG("Reconnecting to ApplicationPool server"); + *pool_ptr = applicationPoolServer->connect(); } return *pool_ptr; } + bool hasModRewrite() { + if (m_hasModRewrite == UNKNOWN) { + if (ap_find_linked_module("mod_rewrite.c")) { + m_hasModRewrite = YES; + } else { + m_hasModRewrite = NO; + } + } + return m_hasModRewrite == YES; + } + + bool hasModDir() { + if (m_hasModDir == UNKNOWN) { + if (ap_find_linked_module("mod_dir.c")) { + m_hasModDir = YES; + } else { + m_hasModDir = NO; + } + } + return m_hasModDir == YES; + } + + bool hasModAutoIndex() { + if (m_hasModAutoIndex == UNKNOWN) { + if (ap_find_linked_module("mod_autoindex.c")) { + m_hasModAutoIndex = YES; + } else { + m_hasModAutoIndex = NO; + } + } + return m_hasModAutoIndex == YES; + } + int reportDocumentRootDeterminationError(request_rec *r) { ap_set_content_type(r, "text/html; charset=UTF-8"); ap_rputs("

Passenger error #1

\n", r); @@ -327,28 +221,322 @@ class Hooks { return OK; } - int reportFileSystemError(request_rec *r, const FileSystemException &e) { - ap_set_content_type(r, "text/html; charset=UTF-8"); - ap_rputs("

Passenger error #2

\n", r); - ap_rputs("An error occurred while trying to access '", r); - ap_rputs(ap_escape_html(r->pool, e.filename().c_str()), r); - ap_rputs("': ", r); - ap_rputs(ap_escape_html(r->pool, e.what()), r); - if (e.code() == EPERM) { - ap_rputs("

", r); - ap_rputs("Apache doesn't have read permissions to that file. ", r); - ap_rputs("Please fix the relevant file permissions.", r); - ap_rputs("

", r); - } - return OK; - } - int reportBusyException(request_rec *r) { ap_custom_response(r, HTTP_SERVICE_UNAVAILABLE, "This website is too busy right now. Please try again later."); return HTTP_SERVICE_UNAVAILABLE; } + /** + * Gather some information about the request and do some preparations. In this method, + * it will be determined whether the request URI should be served statically by Apache + * (in case of static assets or in case there's a page cache file available) or + * whether it should be forwarded to the backend application. + * + * The strategy is as follows: + * + * We check whether Phusion Passenger is enabled for this URI (A). + * If so, then we check whether the following situations are true: + * (B) There is a backend application defined for this URI. + * (C) r->filename already exists. + * (D) There is a page cache file for the URI. + * + * - If A is not true, or if B is not true, or if C is true, then won't do anything. + * Passenger will be disabled during the rest of this request. + * - If D is true, then we first transform r->filename to the page cache file's + * filename, and then we let Apache serve it statically. + * - If D is not true, then we forward the request to the backend application. + * + * @pre The (A) condition must be true. + * @param coreModuleWillBeRun Whether the core.c map_to_storage hook might be called after this. + * @return Whether the Passenger handler hook method should be run. + */ + bool prepareRequest(request_rec *r, DirConfig *config, const char *filename, bool coreModuleWillBeRun = false) { + TRACE_POINT(); + DirectoryMapper mapper(r, config, mstat, config->getStatThrottleRate()); + try { + if (mapper.getBaseURI() == NULL) { + // (B) is not true. + return false; + } + } catch (const FileSystemException &e) { + /* DirectoryMapper tried to examine the filesystem in order + * to autodetect the application type (e.g. by checking whether + * environment.rb exists. But something went wrong, probably + * because of a permission problem. This usually + * means that the user is trying to deploy an application, but + * set the wrong permissions on the relevant folders. + * Later, in the handler hook, we inform the user about this + * problem so that he can either disable Phusion Passenger's + * autodetection routines, or fix the permissions. + */ + apr_pool_userdata_set(new ReportFileSystemError(e), + "Phusion Passenger: error report", + ReportFileSystemError::cleanup, + r->pool); + return true; + } + + /* Save some information for the hook methods that are called later. + * The existance of this note indicates that the URI belongs to a Phusion + * Passenger-served application. + */ + RequestNote *note = new RequestNote(mapper); + note->config = config; + apr_pool_userdata_set(note, "Phusion Passenger", RequestNote::cleanup, r->pool); + + try { + // (B) is true. + FileType fileType = getFileType(filename); + if (fileType == FT_REGULAR) { + // (C) is true. + return false; + } + + // (C) is not true. Check whether (D) is true. + char *pageCacheFile; + /* Only GET requests may hit the page cache. This is + * important because of REST conventions, e.g. + * 'POST /foo' maps to 'FooController#create', + * while 'GET /foo' maps to 'FooController#index'. + * We wouldn't want our page caching support to interfere + * with that. + */ + if (r->method_number == M_GET) { + if (fileType == FT_DIRECTORY) { + size_t len; + + len = strlen(filename); + if (len > 0 && filename[len - 1] == '/') { + pageCacheFile = apr_pstrcat(r->pool, filename, + "index.html", NULL); + } else { + pageCacheFile = apr_pstrcat(r->pool, filename, + ".html", NULL); + } + } else { + pageCacheFile = apr_pstrcat(r->pool, filename, + ".html", NULL); + } + if (!fileExists(pageCacheFile)) { + pageCacheFile = NULL; + } + } else { + pageCacheFile = NULL; + } + if (pageCacheFile != NULL) { + // (D) is true. + r->filename = pageCacheFile; + r->canonical_filename = pageCacheFile; + if (!coreModuleWillBeRun) { + r->finfo.filetype = APR_NOFILE; + ap_set_content_type(r, "text/html"); + ap_directory_walk(r); + ap_file_walk(r); + } + return false; + } else { + // (D) is not true. + note->forwardToBackend = true; + return true; + } + } catch (const FileSystemException &e) { + /* Something went wrong while accessing the directory in which + * r->filename lives. We already know that this URI belongs to + * a backend application, so this error probably means that the + * user set the wrong permissions for his 'public' folder. We + * don't let the handler hook run so that Apache can decide how + * to display the error. + */ + return false; + } + } + + /** + * Most of the high-level logic for forwarding a request to a backend application + * is contained in this method. + */ + int handleRequest(request_rec *r) { + /* Check whether an error occured in prepareRequest() that should be reported + * to the browser. + */ + + // The union is needed in order to be compliant with + // C99/C++'s strict aliasing rules. http://tinyurl.com/g5hgh + union { + ErrorReport *errorReport; + void *pointer; + } u; + + u.errorReport = 0; + apr_pool_userdata_get(&u.pointer, "Phusion Passenger: error report", r->pool); + if (u.errorReport != 0) { + return u.errorReport->report(r); + } + + RequestNote *note = getRequestNote(r); + if (note == 0 || !note->forwardToBackend) { + return DECLINED; + } else if (r->handler != NULL && strcmp(r->handler, "redirect-handler") == 0) { + // mod_rewrite is at work. + return DECLINED; + } + + TRACE_POINT(); + DirConfig *config = note->config; + DirectoryMapper &mapper(note->mapper); + + if (mapper.getPublicDirectory().empty()) { + return reportDocumentRootDeterminationError(r); + } + + int httpStatus = ap_setup_client_block(r, REQUEST_CHUNKED_ERROR); + if (httpStatus != OK) { + return httpStatus; + } + + try { + this_thread::disable_interruption di; + this_thread::disable_syscall_interruption dsi; + apr_bucket_brigade *bb; + apr_bucket *b; + Application::SessionPtr session; + bool expectingUploadData; + shared_ptr uploadData; + + expectingUploadData = ap_should_client_block(r); + if (expectingUploadData && atol(lookupHeader(r, "Content-Length")) + > UPLOAD_ACCELERATION_THRESHOLD) { + uploadData = receiveRequestBody(r); + } + + UPDATE_TRACE_POINT(); + try { + ServerConfig *sconfig = getServerConfig(r->server); + string appRoot(canonicalizePath( + config->getAppRoot(mapper.getPublicDirectory().c_str()) + )); + + session = getApplicationPool()->get(PoolOptions( + appRoot, + true, + sconfig->getDefaultUser(), + mapper.getEnvironment(), + config->getSpawnMethodString(), + mapper.getApplicationTypeString(), + config->frameworkSpawnerTimeout, + config->appSpawnerTimeout, + config->getMaxRequests(), + config->getMemoryLimit(), + config->usingGlobalQueue(), + config->getStatThrottleRate(), + config->getRestartDir() + )); + P_TRACE(3, "Forwarding " << r->uri << " to PID " << session->getPid()); + } catch (const SpawnException &e) { + r->status = 500; + if (e.hasErrorPage()) { + ap_set_content_type(r, "text/html; charset=utf-8"); + ap_rputs(e.getErrorPage().c_str(), r); + return OK; + } else { + throw; + } + } catch (const FileSystemException &e) { + /* The application root cannot be determined. This could + * happen if, for example, the user specified 'RailsBaseURI /foo' + * while there is no filesystem entry called "foo" in the virtual + * host's document root. + */ + return ReportFileSystemError(e).report(r); + } catch (const BusyException &e) { + return reportBusyException(r); + } + + UPDATE_TRACE_POINT(); + session->setReaderTimeout(r->server->timeout / 1000); + session->setWriterTimeout(r->server->timeout / 1000); + sendHeaders(r, session, mapper.getBaseURI()); + if (expectingUploadData) { + if (uploadData != NULL) { + sendRequestBody(r, session, uploadData); + uploadData.reset(); + } else { + sendRequestBody(r, session); + } + } + session->shutdownWriter(); + + UPDATE_TRACE_POINT(); + apr_file_t *readerPipe = NULL; + int reader = session->getStream(); + pid_t backendPid = session->getPid(); + apr_os_pipe_put(&readerPipe, &reader, r->pool); + apr_file_pipe_timeout_set(readerPipe, r->server->timeout); + + bb = apr_brigade_create(r->connection->pool, r->connection->bucket_alloc); + b = passenger_bucket_create(session, readerPipe, r->connection->bucket_alloc); + session.reset(); + APR_BRIGADE_INSERT_TAIL(bb, b); + + b = apr_bucket_eos_create(r->connection->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(bb, b); + + // I know the size because I read util_script.c's source. :-( + char backendData[MAX_STRING_LEN]; + int result = ap_scan_script_header_err_brigade(r, bb, backendData); + if (result == OK) { + // The API documentation for ap_scan_script_err_brigade() says it + // returns HTTP_OK on success, but it actually returns OK. + + // Manually set the Status header because + // ap_scan_script_header_err_brigade() filters it + // out. Some broken HTTP clients depend on the + // Status header for retrieving the HTTP status. + if (!r->status_line && *r->status_line == '\0') { + r->status_line = apr_psprintf(r->pool, + "%d Unknown Status", + r->status); + } + apr_table_setn(r->headers_out, "Status", r->status_line); + + ap_pass_brigade(r->output_filters, bb); + return OK; + } else if (backendData[0] == '\0') { + P_ERROR("Backend process " << backendPid << + " did not return a valid HTTP response. It returned no data."); + apr_table_setn(r->err_headers_out, "Status", "500 Internal Server Error"); + return HTTP_INTERNAL_SERVER_ERROR; + } else { + P_ERROR("Backend process " << backendPid << + " did not return a valid HTTP response. It returned: [" << + backendData << "]"); + apr_table_setn(r->err_headers_out, "Status", "500 Internal Server Error"); + return HTTP_INTERNAL_SERVER_ERROR; + } + + } catch (const thread_interrupted &e) { + P_TRACE(3, "A system call was interrupted during an HTTP request. Apache " + "is probably restarting or shutting down. Backtrace:\n" << + e.backtrace()); + return HTTP_INTERNAL_SERVER_ERROR; + + } catch (const tracable_exception &e) { + P_ERROR("Unexpected error in mod_passenger: " << + e.what() << "\n" << " Backtrace:\n" << e.backtrace()); + return HTTP_INTERNAL_SERVER_ERROR; + + } catch (const exception &e) { + P_ERROR("Unexpected error in mod_passenger: " << + e.what() << "\n" << " Backtrace: not available"); + return HTTP_INTERNAL_SERVER_ERROR; + + } catch (...) { + P_ERROR("An unexpected, unknown error occured in mod_passenger."); + throw; + } + } + /** * Convert an HTTP header name to a CGI environment name. */ @@ -391,32 +579,6 @@ class Hooks { return lookupName(r->subprocess_env, name); } - // This code is a duplicate of what's in util_script.c. We can't use - // r->unparsed_uri because it gets changed if there was a redirect. - char *originalURI(request_rec *r) { - char *first, *last; - - if (r->the_request == NULL) { - return (char *) apr_pcalloc(r->pool, 1); - } - - first = r->the_request; // use the request-line - - while (*first && !apr_isspace(*first)) { - ++first; // skip over the method - } - while (apr_isspace(*first)) { - ++first; // and the space(s) - } - - last = first; - while (*last && !apr_isspace(*last)) { - ++last; // end at next whitespace - } - - return apr_pstrmemdup(r->pool, first, last - first); - } - void inline addHeader(apr_table_t *table, const char *name, const char *value) { if (name != NULL && value != NULL) { apr_table_addn(table, name, value); @@ -441,7 +603,7 @@ class Hooks { addHeader(headers, "REMOTE_PORT", apr_psprintf(r->pool, "%d", r->connection->remote_addr->port)); addHeader(headers, "REMOTE_USER", r->user); addHeader(headers, "REQUEST_METHOD", r->method); - addHeader(headers, "REQUEST_URI", originalURI(r)); + addHeader(headers, "REQUEST_URI", r->unparsed_uri); addHeader(headers, "QUERY_STRING", r->args ? r->args : ""); if (strcmp(baseURI, "/") != 0) { addHeader(headers, "SCRIPT_NAME", baseURI); @@ -518,21 +680,38 @@ class Hooks { } shared_ptr receiveRequestBody(request_rec *r) { + TRACE_POINT(); shared_ptr tempFile(new TempFile()); char buf[1024 * 32]; apr_off_t len; + size_t total_written = 0; while ((len = ap_get_client_block(r, buf, sizeof(buf))) > 0) { size_t written = 0; do { size_t ret = fwrite(buf, 1, len - written, tempFile->handle); - if (ret == 0) { - throw SystemException("An error occured while writing " - "HTTP upload data to a temporary file", - errno); + if (ret <= 0 || fflush(tempFile->handle) == EOF) { + int e = errno; + string message("An error occured while " + "buffering HTTP upload data to " + "a temporary file in "); + message.append(getTempDir()); + if (e == ENOSPC) { + message.append(". Please make sure " + "that this directory has " + "enough disk space for " + "buffering file uploads, " + "or set the 'PassengerTempDir' " + "directive to a directory " + "that has enough disk space."); + throw RuntimeException(message); + } else { + throw SystemException(message, e); + } } written += ret; } while (written < (size_t) len); + total_written += written; } if (len == -1) { throw IOException("An error occurred while receiving HTTP upload data."); @@ -544,8 +723,9 @@ class Hooks { } void sendRequestBody(request_rec *r, Application::SessionPtr &session, shared_ptr &uploadData) { + TRACE_POINT(); rewind(uploadData->handle); - P_DEBUG("Content-Length = " << lookupHeader(r, "Content-Length")); + P_DEBUG("File upload: Content-Length = " << lookupHeader(r, "Content-Length")); while (!feof(uploadData->handle)) { char buf[1024 * 32]; size_t size; @@ -573,6 +753,10 @@ class Hooks { passenger_config_merge_all_servers(pconf, s); ServerConfig *config = getServerConfig(s); Passenger::setLogLevel(config->logLevel); + m_hasModRewrite = UNKNOWN; + m_hasModDir = UNKNOWN; + m_hasModAutoIndex = UNKNOWN; + mstat = cached_multi_file_stat_new(1024); P_DEBUG("Initializing Phusion Passenger..."); ap_add_version_component(pconf, "Phusion_Passenger/" PASSENGER_VERSION); @@ -580,6 +764,22 @@ class Hooks { const char *ruby, *user; string applicationPoolServerExe, spawnServer; + if (config->tempDir != NULL) { + setenv("TMPDIR", config->tempDir, 1); + } else { + unsetenv("TMPDIR"); + } + /* + * As described in the comment in init_module, upon (re)starting + * Apache, the Hooks constructor is called twice. We unset + * PHUSION_PASSENGER_TMP before calling createPassengerTmpDir() + * because we want the temp directory's name to contain the PID + * of the process in which the Hooks constructor was called for + * the second time. + */ + unsetenv("PHUSION_PASSENGER_TMP"); + createPassengerTempDir(); + ruby = (config->ruby != NULL) ? config->ruby : DEFAULT_RUBY_COMMAND; if (config->userSwitching) { user = ""; @@ -623,217 +823,204 @@ class Hooks { pool->setMax(config->maxPoolSize); pool->setMaxPerApp(config->maxInstancesPerApp); pool->setMaxIdleTime(config->poolIdleTime); - pool->setUseGlobalQueue(config->getUseGlobalQueue()); } - int handleRequest(request_rec *r) { + ~Hooks() { + cached_multi_file_stat_free(mstat); + removeDirTree(getPassengerTempDir().c_str()); + } + + int prepareRequestWhenInHighPerformanceMode(request_rec *r) { DirConfig *config = getDirConfig(r); - DirectoryMapper mapper(r, config); - if (mapper.getBaseURI() == NULL || r->filename == NULL || fileExists(r->filename)) { + if (config->isEnabled() && config->highPerformanceMode()) { + if (prepareRequest(r, config, r->filename, true)) { + return OK; + } else { + return DECLINED; + } + } else { return DECLINED; } - - try { - if (mapper.getPublicDirectory().empty()) { - return reportDocumentRootDeterminationError(r); + } + + /** + * This is the hook method for the map_to_storage hook. Apache's final map_to_storage hook + * method (defined in core.c) will do the following: + * + * If r->filename doesn't exist, then it will change the filename to the + * following form: + * + * A/B + * + * A is top-most directory that exists. B is the first filename piece that + * normally follows A. For example, suppose that a website's DocumentRoot + * is /website, on server http://test.com/. Suppose that there's also a + * directory /website/images. + * + * If we access: then r->filename will be: + * http://test.com/foo/bar /website/foo + * http://test.com/foo/bar/baz /website/foo + * http://test.com/images/foo/bar /website/images/foo + * + * We obviously don't want this to happen because it'll interfere with our page + * cache file search code. So here we save the original value of r->filename so + * that we can use it later. + */ + int saveOriginalFilename(request_rec *r) { + apr_table_set(r->notes, "Phusion Passenger: original filename", r->filename); + return DECLINED; + } + + int prepareRequestWhenNotInHighPerformanceMode(request_rec *r) { + DirConfig *config = getDirConfig(r); + if (config->isEnabled()) { + if (config->highPerformanceMode()) { + /* Preparations have already been done in the map_to_storage hook. + * Prevent other modules' fixups hooks from being run. + */ + return OK; + } else { + // core.c's map_to_storage hook will transform the filename, as + // described by saveOriginalFilename(). Here we restore the + // original filename. + const char *filename = apr_table_get(r->notes, "Phusion Passenger: original filename"); + if (filename == NULL) { + return DECLINED; + } else { + prepareRequest(r, config, filename); + return DECLINED; + } } - } catch (const FileSystemException &e) { - return reportFileSystemError(r, e); + } else { + return DECLINED; } - - int httpStatus = ap_setup_client_block(r, REQUEST_CHUNKED_ERROR); - if (httpStatus != OK) { - return httpStatus; + } + + /** + * The default .htaccess provided by on Rails on Rails (that is, before version 2.1.0) + * has the following mod_rewrite rules in it: + * + * RewriteEngine on + * RewriteRule ^$ index.html [QSA] + * RewriteRule ^([^.]+)$ $1.html [QSA] + * RewriteCond %{REQUEST_FILENAME} !-f + * RewriteRule ^(.*)$ dispatch.cgi [QSA,L] + * + * As a result, all requests that do not map to a filename will be redirected to + * dispatch.cgi (or dispatch.fcgi, if the user so specified). We don't want that + * to happen, so before mod_rewrite applies its rules, we save the current state. + * After mod_rewrite has applied its rules, undoRedirectionToDispatchCgi() will + * check whether mod_rewrite attempted to perform an internal redirection to + * dispatch.(f)cgi. If so, then it will revert the state to the way it was before + * mod_rewrite took place. + */ + int saveStateBeforeRewriteRules(request_rec *r) { + RequestNote *note = getRequestNote(r); + if (note != 0 && hasModRewrite()) { + note->handlerBeforeModRewrite = r->handler; + note->filenameBeforeModRewrite = r->filename; + } + return DECLINED; + } + + int undoRedirectionToDispatchCgi(request_rec *r) { + RequestNote *note = getRequestNote(r); + if (note == 0 || !hasModRewrite()) { + return DECLINED; } - try { - this_thread::disable_interruption di; - this_thread::disable_syscall_interruption dsi; - apr_bucket_brigade *bb; - apr_bucket *b; - Application::SessionPtr session; - bool expectingUploadData; - shared_ptr uploadData; - - expectingUploadData = ap_should_client_block(r); - if (expectingUploadData && atol(lookupHeader(r, "Content-Length")) - > UPLOAD_ACCELERATION_THRESHOLD) { - uploadData = receiveRequestBody(r); - } - - try { - const char *defaultUser, *environment, *spawnMethod; - ServerConfig *sconfig; - - sconfig = getServerConfig(r->server); - if (sconfig->defaultUser != NULL) { - defaultUser = sconfig->defaultUser; - } else { - defaultUser = "nobody"; - } - if (mapper.getApplicationType() == DirectoryMapper::RAILS) { - if (config->railsEnv == NULL) { - environment = DEFAULT_RAILS_ENV; - } else { - environment = config->railsEnv; - } - } else if (mapper.getApplicationType() == DirectoryMapper::RACK) { - if (config->rackEnv == NULL) { - environment = DEFAULT_RACK_ENV; - } else { - environment = config->rackEnv; - } - } else { - environment = DEFAULT_WSGI_ENV; - } - if (config->spawnMethod == DirConfig::SM_CONSERVATIVE) { - spawnMethod = "conservative"; - } else { - spawnMethod = "smart"; - } - - session = getApplicationPool()->get( - canonicalizePath(mapper.getPublicDirectory() + "/.."), - true, defaultUser, environment, spawnMethod, - mapper.getApplicationTypeString()); - P_TRACE(3, "Forwarding " << r->uri << " to PID " << session->getPid()); - } catch (const SpawnException &e) { - if (e.hasErrorPage()) { - r->status = 500; - ap_set_content_type(r, "text/html; charset=utf-8"); - ap_rputs(e.getErrorPage().c_str(), r); - return OK; - } else { - throw; - } - } catch (const BusyException &e) { - return reportBusyException(r); - } - sendHeaders(r, session, mapper.getBaseURI()); - if (expectingUploadData) { - if (uploadData != NULL) { - sendRequestBody(r, session, uploadData); - uploadData.reset(); - } else { - sendRequestBody(r, session); + if (r->handler != NULL && strcmp(r->handler, "redirect-handler") == 0) { + // Check whether r->filename looks like "redirect:.../dispatch.(f)cgi" + size_t len = strlen(r->filename); + // 22 == strlen("redirect:/dispatch.cgi") + if (len >= 22 && memcmp(r->filename, "redirect:", 9) == 0 + && (memcmp(r->filename + len - 13, "/dispatch.cgi", 13) == 0 + || memcmp(r->filename + len - 14, "/dispatch.fcgi", 14) == 0)) { + if (note->filenameBeforeModRewrite != NULL) { + r->filename = note->filenameBeforeModRewrite; + r->canonical_filename = note->filenameBeforeModRewrite; + r->handler = note->handlerBeforeModRewrite; } } - session->shutdownWriter(); - - apr_file_t *readerPipe = NULL; - int reader = session->getStream(); - apr_os_pipe_put(&readerPipe, &reader, r->pool); - - bb = apr_brigade_create(r->connection->pool, r->connection->bucket_alloc); - b = passenger_bucket_create(readerPipe, r->connection->bucket_alloc); - APR_BRIGADE_INSERT_TAIL(bb, b); - - b = apr_bucket_eos_create(r->connection->bucket_alloc); - APR_BRIGADE_INSERT_TAIL(bb, b); - - ap_scan_script_header_err_brigade(r, bb, NULL); - ap_pass_brigade(r->output_filters, bb); - - Container *container = new Container(); - container->session = session; - apr_pool_cleanup_register(r->pool, container, Container::cleanup, apr_pool_cleanup_null); - - return OK; - } catch (const thread_interrupted &) { - P_TRACE(3, "A system call was interrupted during an HTTP request. Apache " - "is probably restarting or shutting down."); - return HTTP_INTERNAL_SERVER_ERROR; - } catch (const exception &e) { - ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "*** Unexpected error in Passenger: %s", e.what()); - return HTTP_INTERNAL_SERVER_ERROR; } + return DECLINED; + } + + /** + * mod_dir does the following: + * If r->filename is a directory, and the URI doesn't end with a slash, + * then it will redirect the browser to an URI with a slash. For example, + * if you go to http://foo.com/images, then it will redirect you to + * http://foo.com/images/. + * + * This behavior is undesired. Suppose that there is an ImagesController, + * and there's also a 'public/images' folder used for storing page cache + * files. Then we don't want mod_dir to perform the redirection. + * + * So in startBlockingModDir(), we temporarily change some fields in the + * request structure in order to block mod_dir. In endBlockingModDir() we + * revert those fields to their old value. + */ + int startBlockingModDir(request_rec *r) { + RequestNote *note = getRequestNote(r); + if (note != 0 && hasModDir()) { + note->oldFileType = r->finfo.filetype; + r->finfo.filetype = APR_NOFILE; + } + return DECLINED; + } + + int endBlockingModDir(request_rec *r) { + RequestNote *note = getRequestNote(r); + if (note != 0 && hasModDir()) { + r->finfo.filetype = note->oldFileType; + } + return DECLINED; + } + + /** + * mod_autoindex will try to display a directory index for URIs that map to a directory. + * This is undesired because of page caching semantics. Suppose that a Rails application + * has an ImagesController which has page caching enabled, and thus also a 'public/images' + * directory. When the visitor visits /images we'll want the request to be forwarded to + * the Rails application, instead of displaying a directory index. + * + * So in this hook method, we temporarily change some fields in the request structure + * in order to block mod_autoindex. In endBlockingModAutoIndex(), we restore the request + * structure to its former state. + */ + int startBlockingModAutoIndex(request_rec *r) { + RequestNote *note = getRequestNote(r); + if (note != 0 && hasModAutoIndex()) { + note->handlerBeforeModAutoIndex = r->handler; + r->handler = ""; + } + return DECLINED; + } + + int endBlockingModAutoIndex(request_rec *r) { + RequestNote *note = getRequestNote(r); + if (note != 0 && hasModAutoIndex()) { + r->handler = note->handlerBeforeModAutoIndex; + } + return DECLINED; } - int - mapToStorage(request_rec *r) { + int handleRequestWhenInHighPerformanceMode(request_rec *r) { DirConfig *config = getDirConfig(r); - DirectoryMapper mapper(r, config); - bool forwardToApplication; - - try { - if (mapper.getBaseURI() == NULL || fileExists(r->filename)) { - /* - * fileExists(): - * If the file already exists, serve it directly. - * This is for static assets like .css and .js files. - */ - forwardToApplication = false; - } else if (r->method_number == M_GET) { - char *html_file; - size_t len; - - len = strlen(r->filename); - if (len > 0 && r->filename[len - 1] == '/') { - html_file = apr_pstrcat(r->pool, r->filename, "index.html", NULL); - } else { - html_file = apr_pstrcat(r->pool, r->filename, ".html", NULL); - } - if (fileExists(html_file)) { - /* If a .html version of the URI exists, serve it directly. - * We're essentially accelerating Rails page caching. - */ - r->filename = html_file; - r->canonical_filename = html_file; - forwardToApplication = false; - } else { - forwardToApplication = true; - } - } else { - /* - * Non-GET requests are always forwarded to the application. - * This important because of REST conventions, e.g. - * 'POST /foo' maps to 'FooController.create', - * while 'GET /foo' maps to 'FooController.index'. - * We wouldn't want our page caching support to interfere - * with that. - */ - forwardToApplication = true; - } - - if (forwardToApplication) { - /* Apache's default map_to_storage process does strange - * things with the filename. Suppose that the DocumentRoot - * is /website, on server http://test.com/. If we access - * http://test.com/foo/bar, and /website/foo/bar does not - * exist, then Apache will change the filename to - * /website/foo instead of the expected /website/bar. - * We make sure that doesn't happen. - * - * Incidentally, this also disables mod_rewrite. That is a - * good thing because the default Rails .htaccess file - * interferes with Passenger anyway (it delegates requests - * to the CGI script dispatch.cgi). - */ - if (config->allowModRewrite != DirConfig::ENABLED - && mapper.getApplicationType() == DirectoryMapper::RAILS) { - /* Of course, we only do that if all of the following - * are true: - * - the config allows us to. Some people have complex - * mod_rewrite rules that they don't want to abandon. - * Those people will have to make sure that the Rails - * app's .htaccess doesn't interfere. - * - this is a Rails application. - */ - return OK; - } else if (strcmp(r->uri, mapper.getBaseURI()) == 0) { - /* If the request URI is the application's base URI, - * then we'll want to take over control. Otherwise, - * Apache will show a directory listing. This fixes issue #11. - */ - return OK; - } else { - return DECLINED; - } - } else { - return DECLINED; - } - } catch (const FileSystemException &e) { + if (config->highPerformanceMode()) { + return handleRequest(r); + } else { + return DECLINED; + } + } + + int handleRequestWhenNotInHighPerformanceMode(request_rec *r) { + DirConfig *config = getDirConfig(r); + if (config->highPerformanceMode()) { return DECLINED; + } else { + return handleRequest(r); } } }; @@ -900,9 +1087,10 @@ init_module(apr_pool_t *pconf, apr_pool_t *plog, apr_pool_t *ptemp, server_rec * apr_pool_cleanup_null); return OK; - } catch (const thread_interrupted &) { + } catch (const thread_interrupted &e) { P_TRACE(2, "A system call was interrupted during mod_passenger " - "initialization. Apache might be restarting or shutting down."); + "initialization. Apache might be restarting or shutting " + "down. Backtrace:\n" << e.backtrace()); return DECLINED; } catch (const thread_resource_error &e) { @@ -911,7 +1099,14 @@ init_module(apr_pool_t *pconf, apr_pool_t *plog, apr_pool_t *ptemp, server_rec * lim.rlim_cur = 0; lim.rlim_max = 0; + + /* Solaris does not define the RLIMIT_NPROC limit. Setting it to infinity... */ +#ifdef RLIMIT_NPROC getrlimit(RLIMIT_NPROC, &lim); +#else + lim.rlim_cur = lim.rlim_max = RLIM_INFINITY; +#endif + #ifdef PTHREAD_THREADS_MAX pthread_threads_max = toString(PTHREAD_THREADS_MAX); #else @@ -951,32 +1146,52 @@ init_module(apr_pool_t *pconf, apr_pool_t *plog, apr_pool_t *ptemp, server_rec * } } -static int -handle_request(request_rec *r) { - if (hooks != NULL) { - return hooks->handleRequest(r); - } else { - return DECLINED; +#define DEFINE_REQUEST_HOOK(c_name, cpp_name) \ + static int c_name(request_rec *r) { \ + if (OXT_LIKELY(hooks != NULL)) { \ + return hooks->cpp_name(r); \ + } else { \ + return DECLINED; \ + } \ } -} -static int -map_to_storage(request_rec *r) { - if (hooks != NULL) { - return hooks->mapToStorage(r); - } else { - return DECLINED; - } -} +DEFINE_REQUEST_HOOK(prepare_request_when_in_high_performance_mode, prepareRequestWhenInHighPerformanceMode) +DEFINE_REQUEST_HOOK(save_original_filename, saveOriginalFilename) +DEFINE_REQUEST_HOOK(prepare_request_when_not_in_high_performance_mode, prepareRequestWhenNotInHighPerformanceMode) +DEFINE_REQUEST_HOOK(save_state_before_rewrite_rules, saveStateBeforeRewriteRules) +DEFINE_REQUEST_HOOK(undo_redirection_to_dispatch_cgi, undoRedirectionToDispatchCgi) +DEFINE_REQUEST_HOOK(start_blocking_mod_dir, startBlockingModDir) +DEFINE_REQUEST_HOOK(end_blocking_mod_dir, endBlockingModDir) +DEFINE_REQUEST_HOOK(start_blocking_mod_autoindex, startBlockingModAutoIndex) +DEFINE_REQUEST_HOOK(end_blocking_mod_autoindex, endBlockingModAutoIndex) +DEFINE_REQUEST_HOOK(handle_request_when_in_high_performance_mode, handleRequestWhenInHighPerformanceMode) +DEFINE_REQUEST_HOOK(handle_request_when_not_in_high_performance_mode, handleRequestWhenNotInHighPerformanceMode) + /** * Apache hook registration function. */ void passenger_register_hooks(apr_pool_t *p) { + static const char * const rewrite_module[] = { "mod_rewrite.c", NULL }; + static const char * const dir_module[] = { "mod_dir.c", NULL }; + static const char * const autoindex_module[] = { "mod_autoindex.c", NULL }; + ap_hook_post_config(init_module, NULL, NULL, APR_HOOK_MIDDLE); - ap_hook_map_to_storage(map_to_storage, NULL, NULL, APR_HOOK_FIRST); - ap_hook_handler(handle_request, NULL, NULL, APR_HOOK_MIDDLE); + + ap_hook_map_to_storage(prepare_request_when_in_high_performance_mode, NULL, NULL, APR_HOOK_FIRST); + ap_hook_map_to_storage(save_original_filename, NULL, NULL, APR_HOOK_LAST); + + ap_hook_fixups(prepare_request_when_not_in_high_performance_mode, NULL, rewrite_module, APR_HOOK_FIRST); + ap_hook_fixups(save_state_before_rewrite_rules, NULL, rewrite_module, APR_HOOK_LAST); + ap_hook_fixups(undo_redirection_to_dispatch_cgi, rewrite_module, NULL, APR_HOOK_FIRST); + ap_hook_fixups(start_blocking_mod_dir, NULL, dir_module, APR_HOOK_MIDDLE); + ap_hook_fixups(end_blocking_mod_dir, dir_module, NULL, APR_HOOK_MIDDLE); + + ap_hook_handler(handle_request_when_in_high_performance_mode, NULL, NULL, APR_HOOK_FIRST); + ap_hook_handler(start_blocking_mod_autoindex, NULL, autoindex_module, APR_HOOK_LAST); + ap_hook_handler(end_blocking_mod_autoindex, autoindex_module, NULL, APR_HOOK_FIRST); + ap_hook_handler(handle_request_when_not_in_high_performance_mode, NULL, NULL, APR_HOOK_LAST); } /** diff --git a/ext/apache2/LICENSE-CNRI.TXT b/ext/apache2/LICENSE-CNRI.TXT index ece10b0b..2778953a 100644 --- a/ext/apache2/LICENSE-CNRI.TXT +++ b/ext/apache2/LICENSE-CNRI.TXT @@ -2,6 +2,21 @@ A few functions in ext/apache2/Hooks.cpp are based on the source code of mod_scgi version 1.9. Its license is included in this file. Please note that these licensing terms *only* encompass those few functions, and not Passenger as a whole. + +The functions which are based on mod_scgi's code are as follows: +- Hooks::prepareRequest(). Although our version looks nothing like the + original, the idea of checking for the file's existance from the + map_to_storage/fixups hook is inspired by mod_scgi's code. +- Hooks::handleRequest(). Although our version looks nothing like the original, + the idea of passing the backend process's socket file descriptor up to the + bucket brigade chain is inspired by mod_scgi's code. +- Hooks::http2env(), Hooks::lookupName(), Hooks::lookupHeader(), + Hooks::lookupEnv(), Hooks::addHeader(): Copied from mod_scgi's functions that + are named similarly. Slightly modified to make the coding style consistent + with the rest of Phusion Passenger. +- Hooks::sendHeaders(): Based for the most part on mod_scgi's send_headers() + function. + ------------------------------------------------------------------------ CNRI OPEN SOURCE LICENSE AGREEMENT diff --git a/ext/apache2/Logging.h b/ext/apache2/Logging.h index 739b4868..8e90320e 100644 --- a/ext/apache2/Logging.h +++ b/ext/apache2/Logging.h @@ -24,6 +24,7 @@ #include #include #include +#include #include namespace Passenger { @@ -39,24 +40,38 @@ void setLogLevel(unsigned int value); void setDebugFile(const char *logFile = NULL); /** - * Write the given expression to the log stream. + * Write the given expression to the given stream. + * + * @param expr The expression to write. + * @param stream A pointer to an object that accepts the '<<' operator. */ -#define P_LOG(expr) \ +#define P_LOG_TO(expr, stream) \ do { \ - if (Passenger::_logStream != 0) { \ - time_t the_time = time(NULL); \ - struct tm *the_tm = localtime(&the_time); \ - char datetime_buf[60]; \ - struct timeval tv; \ - strftime(datetime_buf, sizeof(datetime_buf), "%x %H:%M:%S", the_tm); \ + if (stream != 0) { \ + time_t the_time; \ + struct tm *the_tm; \ + char datetime_buf[60]; \ + struct timeval tv; \ + std::stringstream sstream; \ + \ + the_time = time(NULL); \ + the_tm = localtime(&the_time); \ + strftime(datetime_buf, sizeof(datetime_buf), "%F %H:%M:%S", the_tm); \ gettimeofday(&tv, NULL); \ - *Passenger::_logStream << \ + sstream << \ "[ pid=" << getpid() << " file=" << __FILE__ << ":" << __LINE__ << \ " time=" << datetime_buf << "." << (tv.tv_usec / 1000) << " ]:" << \ - "\n " << expr << std::endl; \ + "\n " << expr << std::endl; \ + *stream << sstream.str(); \ + stream->flush(); \ } \ } while (false) +/** + * Write the given expression to the log stream. + */ +#define P_LOG(expr) P_LOG_TO(expr, Passenger::_logStream) + /** * Write the given expression, which represents a warning, * to the log stream. @@ -79,18 +94,7 @@ void setDebugFile(const char *logFile = NULL); #define P_TRACE(level, expr) \ do { \ if (Passenger::_logLevel >= level) { \ - if (Passenger::_debugStream != 0) { \ - time_t the_time = time(NULL); \ - struct tm *the_tm = localtime(&the_time); \ - char datetime_buf[60]; \ - struct timeval tv; \ - strftime(datetime_buf, sizeof(datetime_buf), "%x %H:%M:%S", the_tm); \ - gettimeofday(&tv, NULL); \ - *Passenger::_debugStream << \ - "[ pid=" << getpid() << " file=" << __FILE__ << ":" << __LINE__ << \ - " time=" << datetime_buf << "." << (tv.tv_usec / 1000) << " ]:" << \ - "\n " << expr << std::endl; \ - } \ + P_LOG_TO(expr, Passenger::_debugStream); \ } \ } while (false) diff --git a/ext/apache2/MessageChannel.h b/ext/apache2/MessageChannel.h index 740ef430..05e99069 100644 --- a/ext/apache2/MessageChannel.h +++ b/ext/apache2/MessageChannel.h @@ -20,6 +20,8 @@ #ifndef _PASSENGER_MESSAGE_CHANNEL_H_ #define _PASSENGER_MESSAGE_CHANNEL_H_ +#include + #include #include #include @@ -31,14 +33,25 @@ #include #include #include +#ifdef __OpenBSD__ + // OpenBSD needs this for 'struct iovec'. Apparently it isn't + // always included by unistd.h and sys/types.h. + #include +#endif +#if !APR_HAVE_IOVEC + // We don't want apr_want.h to redefine 'struct iovec'. + // http://tinyurl.com/b6aatw + #undef APR_HAVE_IOVEC + #define APR_HAVE_IOVEC 1 +#endif -#include "System.h" #include "Exceptions.h" #include "Utils.h" namespace Passenger { using namespace std; +using namespace oxt; /** * Convenience class for I/O operations on file descriptors. @@ -99,6 +112,11 @@ class MessageChannel { private: const static char DELIMITER = '\0'; int fd; + + #ifdef __OpenBSD__ + typedef u_int32_t uint32_t; + typedef u_int16_t uint16_t; + #endif public: /** @@ -127,7 +145,7 @@ class MessageChannel { */ void close() { if (fd != -1) { - int ret = InterruptableCalls::close(fd); + int ret = syscalls::close(fd); if (ret == -1) { throw SystemException("Cannot close file descriptor", errno); } @@ -139,14 +157,18 @@ class MessageChannel { * Send an array message, which consists of the given elements, over the underlying * file descriptor. * - * @param args The message elements. + * @param args An object which contains the message elements. This object must + * support STL-style iteration, and each iterator must have an + * std::string as value. Use the StringArrayType and + * StringArrayConstIteratorType template parameters to specify the exact type names. * @throws SystemException An error occured while writing the data to the file descriptor. * @throws boost::thread_interrupted * @pre None of the message elements may contain a NUL character ('\\0'). * @see read(), write(const char *, ...) */ - void write(const list &args) { - list::const_iterator it; + template + void write(const StringArrayType &args) { + StringArrayConstIteratorType it; string data; uint16_t dataSize = 0; @@ -164,6 +186,34 @@ class MessageChannel { writeRaw(data); } + /** + * Send an array message, which consists of the given elements, over the underlying + * file descriptor. + * + * @param args The message elements. + * @throws SystemException An error occured while writing the data to the file descriptor. + * @throws boost::thread_interrupted + * @pre None of the message elements may contain a NUL character ('\\0'). + * @see read(), write(const char *, ...) + */ + void write(const list &args) { + write, list::const_iterator>(args); + } + + /** + * Send an array message, which consists of the given elements, over the underlying + * file descriptor. + * + * @param args The message elements. + * @throws SystemException An error occured while writing the data to the file descriptor. + * @throws boost::thread_interrupted + * @pre None of the message elements may contain a NUL character ('\\0'). + * @see read(), write(const char *, ...) + */ + void write(const vector &args) { + write, vector::const_iterator>(args); + } + /** * Send an array message, which consists of the given strings, over the underlying * file descriptor. @@ -237,7 +287,7 @@ class MessageChannel { ssize_t ret; unsigned int written = 0; do { - ret = InterruptableCalls::write(fd, data + written, size - written); + ret = syscalls::write(fd, data + written, size - written); if (ret == -1) { throw SystemException("write() failed", errno); } else { @@ -273,7 +323,7 @@ class MessageChannel { struct msghdr msg; struct iovec vec; char dummy[1]; - #ifdef __APPLE__ + #if defined(__APPLE__) || defined(__SOLARIS__) struct { struct cmsghdr header; int fd; @@ -301,7 +351,7 @@ class MessageChannel { control_header = CMSG_FIRSTHDR(&msg); control_header->cmsg_level = SOL_SOCKET; control_header->cmsg_type = SCM_RIGHTS; - #ifdef __APPLE__ + #if defined(__APPLE__) || defined(__SOLARIS__) control_header->cmsg_len = sizeof(control_data); control_data.fd = fileDescriptor; #else @@ -309,7 +359,7 @@ class MessageChannel { memcpy(CMSG_DATA(control_header), &fileDescriptor, sizeof(int)); #endif - ret = InterruptableCalls::sendmsg(fd, &msg, 0); + ret = syscalls::sendmsg(fd, &msg, 0); if (ret == -1) { throw SystemException("Cannot send file descriptor with sendmsg()", errno); } @@ -331,7 +381,7 @@ class MessageChannel { unsigned int alreadyRead = 0; do { - ret = InterruptableCalls::read(fd, (char *) &size + alreadyRead, sizeof(size) - alreadyRead); + ret = syscalls::read(fd, (char *) &size + alreadyRead, sizeof(size) - alreadyRead); if (ret == -1) { throw SystemException("read() failed", errno); } else if (ret == 0) { @@ -346,7 +396,7 @@ class MessageChannel { buffer.reserve(size); while (buffer.size() < size) { char tmp[1024 * 8]; - ret = InterruptableCalls::read(fd, tmp, min(size - buffer.size(), sizeof(tmp))); + ret = syscalls::read(fd, tmp, min(size - buffer.size(), sizeof(tmp))); if (ret == -1) { throw SystemException("read() failed", errno); } else if (ret == 0) { @@ -421,7 +471,7 @@ class MessageChannel { unsigned int alreadyRead = 0; while (alreadyRead < size) { - ret = InterruptableCalls::read(fd, (char *) buf + alreadyRead, size - alreadyRead); + ret = syscalls::read(fd, (char *) buf + alreadyRead, size - alreadyRead); if (ret == -1) { throw SystemException("read() failed", errno); } else if (ret == 0) { @@ -449,7 +499,7 @@ class MessageChannel { struct msghdr msg; struct iovec vec; char dummy[1]; - #ifdef __APPLE__ + #if defined(__APPLE__) || defined(__SOLARIS__) // File descriptor passing macros (CMSG_*) seem to be broken // on 64-bit MacOS X. This structure works around the problem. struct { @@ -477,7 +527,7 @@ class MessageChannel { msg.msg_controllen = sizeof(control_data); msg.msg_flags = 0; - ret = InterruptableCalls::recvmsg(fd, &msg, 0); + ret = syscalls::recvmsg(fd, &msg, 0); if (ret == -1) { throw SystemException("Cannot read file descriptor with recvmsg()", errno); } @@ -488,12 +538,71 @@ class MessageChannel { || control_header->cmsg_type != SCM_RIGHTS) { throw IOException("No valid file descriptor received."); } - #ifdef __APPLE__ + #if defined(__APPLE__) || defined(__SOLARIS__) return control_data.fd; #else return *((int *) CMSG_DATA(control_header)); #endif } + + /** + * Set the timeout value for reading data from this channel. + * If no data can be read within the timeout period, then a + * SystemException will be thrown by one of the read methods, + * with error code EAGAIN or EWOULDBLOCK. + * + * @param msec The timeout, in milliseconds. If 0 is given, + * there will be no timeout. + * @throws SystemException Cannot set the timeout. + */ + void setReadTimeout(unsigned int msec) { + // See the comment for setWriteTimeout(). + struct timeval tv; + int ret; + + tv.tv_sec = msec / 1000; + tv.tv_usec = msec % 1000 * 1000; + ret = syscalls::setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, + &tv, sizeof(tv)); + #ifndef __SOLARIS__ + // SO_RCVTIMEO is unimplemented and retuns an error on Solaris + // 9 and 10 SPARC. Seems to work okay without it. + if (ret == -1) { + throw SystemException("Cannot set read timeout for socket", errno); + } + #endif + } + + /** + * Set the timeout value for writing data to this channel. + * If no data can be written within the timeout period, then a + * SystemException will be thrown, with error code EAGAIN or + * EWOULDBLOCK. + * + * @param msec The timeout, in milliseconds. If 0 is given, + * there will be no timeout. + * @throws SystemException Cannot set the timeout. + */ + void setWriteTimeout(unsigned int msec) { + // People say that SO_RCVTIMEO/SO_SNDTIMEO are unreliable and + // not well-implemented on all platforms. + // http://www.developerweb.net/forum/archive/index.php/t-3439.html + // That's why we use APR's timeout facilities as well (see Hooks.cpp). + struct timeval tv; + int ret; + + tv.tv_sec = msec / 1000; + tv.tv_usec = msec % 1000 * 1000; + ret = syscalls::setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, + &tv, sizeof(tv)); + #ifndef __SOLARIS__ + // SO_SNDTIMEO is unimplemented and returns an error on Solaris + // 9 and 10 SPARC. Seems to work okay without it. + if (ret == -1) { + throw SystemException("Cannot set read timeout for socket", errno); + } + #endif + } }; } // namespace Passenger diff --git a/ext/apache2/PoolOptions.h b/ext/apache2/PoolOptions.h new file mode 100644 index 00000000..79f805fd --- /dev/null +++ b/ext/apache2/PoolOptions.h @@ -0,0 +1,283 @@ +/* + * Phusion Passenger - http://www.modrails.com/ + * Copyright (C) 2008 Phusion + * + * Phusion Passenger is a trademark of Hongli Lai & Ninh Bui. + * + * 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. + */ +#ifndef _PASSENGER_SPAWN_OPTIONS_H_ +#define _PASSENGER_SPAWN_OPTIONS_H_ + +#include +#include "Utils.h" + +namespace Passenger { + +using namespace std; + +/** + * This struct encapsulates information for ApplicationPool::get() and for + * SpawnManager::spawn(), such as which application is to be spawned. + * + *

Notes on privilege lowering support

+ * + * If lowerPrivilege is true, then it will be attempt to + * switch the spawned application instance to the user who owns the + * application's config/environment.rb, and to the default + * group of that user. + * + * If that user doesn't exist on the system, or if that user is root, + * then it will be attempted to switch to the username given by + * lowestUser (and to the default group of that user). + * If lowestUser doesn't exist either, or if switching user failed + * (because the spawn server process does not have the privilege to do so), + * then the application will be spawned anyway, without reporting an error. + * + * It goes without saying that lowering privilege is only possible if + * the spawn server is running as root (and thus, by induction, that + * Phusion Passenger and Apache's control process are also running as root). + * Note that if Apache is listening on port 80, then its control process must + * be running as root. See "doc/Security of user switching.txt" for + * a detailed explanation. + */ +struct PoolOptions { + /** + * The root directory of the application to spawn. In case of a Ruby on Rails + * application, this is the folder that contains 'app/', 'public/', 'config/', + * etc. This must be a valid directory, but the path does not have to be absolute. + */ + string appRoot; + + /** Whether to lower the application's privileges. */ + bool lowerPrivilege; + + /** + * The user to fallback to if lowering privilege fails. + */ + string lowestUser; + + /** + * The RAILS_ENV/RACK_ENV environment that should be used. May not be an + * empty string. + */ + string environment; + + /** + * The spawn method to use. Either "smart" or "conservative". See the Ruby + * class SpawnManager for details. + */ + string spawnMethod; + + /** The application type. Either "rails", "rack" or "wsgi". */ + string appType; + + /** + * The idle timeout, in seconds, of Rails framework spawners. + * A timeout of 0 means that the framework spawner should never idle timeout. A timeout + * of -1 means that the default timeout value should be used. + * + * For more details about Rails framework spawners, please + * read the documentation on the Railz::FrameworkSpawner + * Ruby class. + */ + long frameworkSpawnerTimeout; + + /** + * The idle timeout, in seconds, of Rails application spawners. + * A timeout of 0 means that the application spawner should never idle timeout. A timeout + * of -1 means that the default timeout value should be used. + * + * For more details about Rails application spawners, please + * read the documentation on the Railz::ApplicationSpawner + * Ruby class. + */ + long appSpawnerTimeout; + + /** + * The maximum number of requests that the spawned application may process + * before exiting. A value of 0 means unlimited. + */ + unsigned long maxRequests; + + /** + * The maximum amount of memory (in MB) the spawned application may use. + * A value of 0 means unlimited. + */ + unsigned long memoryLimit; + + /** + * Whether to use a global queue instead of a per-backend process + * queue. This option is only used by ApplicationPool::get(). + * + * If enabled, when all backend processes are active, get() will + * wait until there's at least one backend process that's idle, instead + * of queuing the request into a random process's private queue. + * This is especially useful if a website has one or more long-running + * requests. + */ + bool useGlobalQueue; + + /** + * A throttling rate for file stats. When set to a non-zero value N, + * restart.txt and other files which are usually stat()ted on every + * ApplicationPool::get() call will be stat()ed at most every N seconds. + */ + unsigned long statThrottleRate; + + /** + * The directory which contains restart.txt and always_restart.txt. + * An empty string means that the default directory should be used. + */ + string restartDir; + + /** + * Creates a new PoolOptions object with the default values filled in. + * One must still set appRoot manually, after having used this constructor. + */ + PoolOptions() { + lowerPrivilege = true; + lowestUser = "nobody"; + environment = "production"; + spawnMethod = "smart"; + appType = "rails"; + frameworkSpawnerTimeout = -1; + appSpawnerTimeout = -1; + maxRequests = 0; + memoryLimit = 0; + useGlobalQueue = false; + statThrottleRate = 0; + } + + /** + * Creates a new PoolOptions object with the given values. + */ + PoolOptions(const string &appRoot, + bool lowerPrivilege = true, + const string &lowestUser = "nobody", + const string &environment = "production", + const string &spawnMethod = "smart", + const string &appType = "rails", + long frameworkSpawnerTimeout = -1, + long appSpawnerTimeout = -1, + unsigned long maxRequests = 0, + unsigned long memoryLimit = 0, + bool useGlobalQueue = false, + unsigned long statThrottleRate = 0, + const string &restartDir = "" + ) { + this->appRoot = appRoot; + this->lowerPrivilege = lowerPrivilege; + this->lowestUser = lowestUser; + this->environment = environment; + this->spawnMethod = spawnMethod; + this->appType = appType; + this->frameworkSpawnerTimeout = frameworkSpawnerTimeout; + this->appSpawnerTimeout = appSpawnerTimeout; + this->maxRequests = maxRequests; + this->memoryLimit = memoryLimit; + this->useGlobalQueue = useGlobalQueue; + this->statThrottleRate = statThrottleRate; + this->restartDir = restartDir; + } + + /** + * Creates a new PoolOptions object from the given string vector. + * This vector contains information that's written to by toVector(). + * + * For example: + * @code + * PoolOptions options(...); + * vector vec; + * + * vec.push_back("my"); + * vec.push_back("data"); + * options.toVector(vec); // PoolOptions information will start at index 2. + * + * PoolOptions copy(vec, 2); + * @endcode + * + * @param vec The vector containing spawn options information. + * @param startIndex The index in vec at which the information starts. + */ + PoolOptions(const vector &vec, unsigned int startIndex = 0) { + appRoot = vec[startIndex + 1]; + lowerPrivilege = vec[startIndex + 3] == "true"; + lowestUser = vec[startIndex + 5]; + environment = vec[startIndex + 7]; + spawnMethod = vec[startIndex + 9]; + appType = vec[startIndex + 11]; + frameworkSpawnerTimeout = atol(vec[startIndex + 13]); + appSpawnerTimeout = atol(vec[startIndex + 15]); + maxRequests = atol(vec[startIndex + 17]); + memoryLimit = atol(vec[startIndex + 19]); + useGlobalQueue = vec[startIndex + 21] == "true"; + statThrottleRate = atol(vec[startIndex + 23]); + restartDir = vec[startIndex + 25]; + } + + /** + * Append the information in this PoolOptions object to the given + * string vector. The resulting array could, for example, be used + * as a message to be sent to the spawn server. + */ + void toVector(vector &vec) const { + if (vec.capacity() < vec.size() + 10) { + vec.reserve(vec.size() + 10); + } + appendKeyValue (vec, "app_root", appRoot); + appendKeyValue (vec, "lower_privilege", lowerPrivilege ? "true" : "false"); + appendKeyValue (vec, "lowest_user", lowestUser); + appendKeyValue (vec, "environment", environment); + appendKeyValue (vec, "spawn_method", spawnMethod); + appendKeyValue (vec, "app_type", appType); + appendKeyValue2(vec, "framework_spawner_timeout", frameworkSpawnerTimeout); + appendKeyValue2(vec, "app_spawner_timeout", appSpawnerTimeout); + appendKeyValue3(vec, "max_requests", maxRequests); + appendKeyValue3(vec, "memory_limit", memoryLimit); + appendKeyValue (vec, "use_global_queue", useGlobalQueue ? "true" : "false"); + appendKeyValue3(vec, "stat_throttle_rate", statThrottleRate); + appendKeyValue (vec, "restart_dir", restartDir); + } + +private: + static inline void + appendKeyValue(vector &vec, const char *key, const string &value) { + vec.push_back(key); + vec.push_back(const_cast(value)); + } + + static inline void + appendKeyValue(vector &vec, const char *key, const char *value) { + vec.push_back(key); + vec.push_back(value); + } + + static inline void + appendKeyValue2(vector &vec, const char *key, long value) { + vec.push_back(key); + vec.push_back(toString(value)); + } + + static inline void + appendKeyValue3(vector &vec, const char *key, unsigned long value) { + vec.push_back(key); + vec.push_back(toString(value)); + } +}; + +} // namespace Passenger + +#endif /* _PASSENGER_SPAWN_OPTIONS_H_ */ + diff --git a/ext/apache2/SpawnManager.h b/ext/apache2/SpawnManager.h index b48e43a4..71b4c1c5 100644 --- a/ext/apache2/SpawnManager.h +++ b/ext/apache2/SpawnManager.h @@ -24,6 +24,8 @@ #include #include #include +#include +#include #include #include @@ -33,19 +35,21 @@ #include #include #include +#include #include #include #include "Application.h" +#include "PoolOptions.h" #include "MessageChannel.h" #include "Exceptions.h" #include "Logging.h" -#include "System.h" namespace Passenger { using namespace std; using namespace boost; +using namespace oxt; /** * @brief Spawning of Ruby on Rails/Rack application instances. @@ -89,7 +93,7 @@ class SpawnManager { string rubyCommand; string user; - mutex lock; + boost::mutex lock; MessageChannel channel; pid_t pid; @@ -103,30 +107,34 @@ class SpawnManager { * @throws IOException The specified log file could not be opened. */ void restartServer() { + TRACE_POINT(); if (pid != 0) { + UPDATE_TRACE_POINT(); channel.close(); // Wait at most 5 seconds for the spawn server to exit. // If that doesn't work, kill it, then wait at most 5 seconds // for it to exit. - time_t begin = InterruptableCalls::time(NULL); + time_t begin = syscalls::time(NULL); bool done = false; - while (!done && InterruptableCalls::time(NULL) - begin < 5) { - if (InterruptableCalls::waitpid(pid, NULL, WNOHANG) > 0) { + while (!done && syscalls::time(NULL) - begin < 5) { + if (syscalls::waitpid(pid, NULL, WNOHANG) > 0) { done = true; } else { - InterruptableCalls::usleep(100000); + syscalls::usleep(100000); } } + UPDATE_TRACE_POINT(); if (!done) { + UPDATE_TRACE_POINT(); P_TRACE(2, "Spawn server did not exit in time, killing it..."); - InterruptableCalls::kill(pid, SIGTERM); - begin = InterruptableCalls::time(NULL); - while (InterruptableCalls::time(NULL) - begin < 5) { - if (InterruptableCalls::waitpid(pid, NULL, WNOHANG) > 0) { + syscalls::kill(pid, SIGTERM); + begin = syscalls::time(NULL); + while (syscalls::time(NULL) - begin < 5) { + if (syscalls::waitpid(pid, NULL, WNOHANG) > 0) { break; } else { - InterruptableCalls::usleep(100000); + syscalls::usleep(100000); } } P_TRACE(2, "Spawn server has exited."); @@ -138,11 +146,11 @@ class SpawnManager { FILE *logFileHandle = NULL; serverNeedsRestart = true; - if (InterruptableCalls::socketpair(AF_UNIX, SOCK_STREAM, 0, fds) == -1) { + if (syscalls::socketpair(AF_UNIX, SOCK_STREAM, 0, fds) == -1) { throw SystemException("Cannot create a Unix socket", errno); } if (!logFile.empty()) { - logFileHandle = InterruptableCalls::fopen(logFile.c_str(), "a"); + logFileHandle = syscalls::fopen(logFile.c_str(), "a"); if (logFileHandle == NULL) { string message("Cannot open log file '"); message.append(logFile); @@ -151,7 +159,8 @@ class SpawnManager { } } - pid = InterruptableCalls::fork(); + UPDATE_TRACE_POINT(); + pid = syscalls::fork(); if (pid == 0) { if (!logFile.empty()) { dup2(fileno(logFileHandle), STDERR_FILENO); @@ -168,6 +177,14 @@ class SpawnManager { if (!user.empty()) { struct passwd *entry = getpwnam(user.c_str()); if (entry != NULL) { + if (initgroups(user.c_str(), entry->pw_gid) != 0) { + int e = errno; + fprintf(stderr, "*** Passenger: cannot set supplementary " + "groups for user %s: %s (%d)\n", + user.c_str(), + strerror(e), + e); + } if (setgid(entry->pw_gid) != 0) { int e = errno; fprintf(stderr, "*** Passenger: cannot run spawn " @@ -202,33 +219,35 @@ class SpawnManager { // This argument is ignored by the spawn server. This works on some // systems, such as Ubuntu Linux. " ", - NULL); + (char *) NULL); int e = errno; - fprintf(stderr, "*** Passenger ERROR: Could not start the spawn server: %s: %s (%d)\n", + fprintf(stderr, "*** Passenger ERROR (%s:%d):\n" + "Could not start the spawn server: %s: %s (%d)\n", + __FILE__, __LINE__, rubyCommand.c_str(), strerror(e), e); fflush(stderr); _exit(1); } else if (pid == -1) { int e = errno; - InterruptableCalls::close(fds[0]); - InterruptableCalls::close(fds[1]); + syscalls::close(fds[0]); + syscalls::close(fds[1]); if (logFileHandle != NULL) { - InterruptableCalls::fclose(logFileHandle); + syscalls::fclose(logFileHandle); } pid = 0; throw SystemException("Unable to fork a process", e); } else { - InterruptableCalls::close(fds[1]); + syscalls::close(fds[1]); if (!logFile.empty()) { - InterruptableCalls::fclose(logFileHandle); + syscalls::fclose(logFileHandle); } channel = MessageChannel(fds[0]); serverNeedsRestart = false; #ifdef TESTING_SPAWN_MANAGER if (nextRestartShouldFail) { - InterruptableCalls::kill(pid, SIGTERM); - InterruptableCalls::usleep(500000); + syscalls::kill(pid, SIGTERM); + syscalls::usleep(500000); } #endif } @@ -237,41 +256,26 @@ class SpawnManager { /** * Send the spawn command to the spawn server. * - * @param appRoot The application root of the application to spawn. - * @param lowerPrivilege Whether to lower the application's privileges. - * @param lowestUser The user to fallback to if lowering privilege fails. - * @param environment The RAILS_ENV/RACK_ENV environment that should be used. - * @param spawnMethod The spawn method to use. - * @param appType The application type. + * @param PoolOptions The spawn options to use. * @return An Application smart pointer, representing the spawned application. * @throws SpawnException Something went wrong. */ - ApplicationPtr sendSpawnCommand( - const string &appRoot, - bool lowerPrivilege, - const string &lowestUser, - const string &environment, - const string &spawnMethod, - const string &appType - ) { + ApplicationPtr sendSpawnCommand(const PoolOptions &PoolOptions) { + TRACE_POINT(); vector args; int ownerPipe; try { - channel.write("spawn_application", - appRoot.c_str(), - (lowerPrivilege) ? "true" : "false", - lowestUser.c_str(), - environment.c_str(), - spawnMethod.c_str(), - appType.c_str(), - NULL); + args.push_back("spawn_application"); + PoolOptions.toVector(args); + channel.write(args); } catch (const SystemException &e) { throw SpawnException(string("Could not write 'spawn_application' " "command to the spawn server: ") + e.sys()); } try { + UPDATE_TRACE_POINT(); // Read status. if (!channel.read(args)) { throw SpawnException("The spawn server has exited unexpectedly."); @@ -299,6 +303,7 @@ class SpawnManager { throw SpawnException(string("Could not read from the spawn server: ") + e.sys()); } + UPDATE_TRACE_POINT(); try { ownerPipe = channel.readFileDescriptor(); } catch (const SystemException &e) { @@ -312,14 +317,15 @@ class SpawnManager { } if (args.size() != 3) { - InterruptableCalls::close(ownerPipe); + UPDATE_TRACE_POINT(); + syscalls::close(ownerPipe); throw SpawnException("The spawn server sent an invalid message."); } pid_t pid = atoi(args[0]); - bool usingAbstractNamespace = args[2] == "true"; - if (!usingAbstractNamespace) { + UPDATE_TRACE_POINT(); + if (args[2] == "unix") { int ret; do { ret = chmod(args[1].c_str(), S_IRUSR | S_IWUSR); @@ -328,18 +334,16 @@ class SpawnManager { ret = chown(args[1].c_str(), getuid(), getgid()); } while (ret == -1 && errno == EINTR); } - return ApplicationPtr(new Application(appRoot, pid, args[1], - usingAbstractNamespace, ownerPipe)); + return ApplicationPtr(new Application(PoolOptions.appRoot, + pid, args[1], args[2], ownerPipe)); } /** * @throws boost::thread_interrupted */ ApplicationPtr - handleSpawnException(const SpawnException &e, const string &appRoot, - bool lowerPrivilege, const string &lowestUser, - const string &environment, const string &spawnMethod, - const string &appType) { + handleSpawnException(const SpawnException &e, const PoolOptions &PoolOptions) { + TRACE_POINT(); bool restarted; try { P_DEBUG("Spawn server died. Attempting to restart it..."); @@ -355,8 +359,7 @@ class SpawnManager { restarted = false; } if (restarted) { - return sendSpawnCommand(appRoot, lowerPrivilege, lowestUser, - environment, spawnMethod, appType); + return sendSpawnCommand(PoolOptions); } else { throw SpawnException("The spawn server died unexpectedly, and restarting it failed."); } @@ -369,6 +372,7 @@ class SpawnManager { * @throws SystemException Something went wrong. */ void sendReloadCommand(const string &appRoot) { + TRACE_POINT(); try { channel.write("reload", appRoot.c_str(), NULL); } catch (const SystemException &e) { @@ -378,6 +382,7 @@ class SpawnManager { } void handleReloadException(const SystemException &e, const string &appRoot) { + TRACE_POINT(); bool restarted; try { P_DEBUG("Spawn server died. Attempting to restart it..."); @@ -434,6 +439,7 @@ class SpawnManager { const string &logFile = "", const string &rubyCommand = "ruby", const string &user = "") { + TRACE_POINT(); this->spawnServerCommand = spawnServerCommand; this->logFile = logFile; this->rubyCommand = rubyCommand; @@ -454,76 +460,45 @@ class SpawnManager { } ~SpawnManager() throw() { + TRACE_POINT(); if (pid != 0) { + UPDATE_TRACE_POINT(); this_thread::disable_interruption di; this_thread::disable_syscall_interruption dsi; P_TRACE(2, "Shutting down spawn manager (PID " << pid << ")."); channel.close(); - InterruptableCalls::waitpid(pid, NULL, 0); + syscalls::waitpid(pid, NULL, 0); P_TRACE(2, "Spawn manager exited."); } } /** - * Spawn a new instance of a Ruby on Rails or Rack application. + * Spawn a new instance of an application. Spawning details are to be passed + * via the PoolOptions parameter. * * If the spawn server died during the spawning process, then the server * will be automatically restarted, and another spawn attempt will be made. * If restarting the server fails, or if the second spawn attempt fails, * then an exception will be thrown. * - * If lowerPrivilege is true, then it will be attempt to - * switch the spawned application instance to the user who owns the - * application's config/environment.rb, and to the default - * group of that user. - * - * If that user doesn't exist on the system, or if that user is root, - * then it will be attempted to switch to the username given by - * lowestUser (and to the default group of that user). - * If lowestUser doesn't exist either, or if switching user failed - * (because the spawn server process does not have the privilege to do so), - * then the application will be spawned anyway, without reporting an error. - * - * It goes without saying that lowering privilege is only possible if - * the spawn server is running as root (and thus, by induction, that - * Passenger and Apache's control process are also running as root). - * Note that if Apache is listening on port 80, then its control process must - * be running as root. See "doc/Security of user switching.txt" for - * a detailed explanation. - * - * @param appRoot The application root of a RoR application, i.e. the folder that - * contains 'app/', 'public/', 'config/', etc. This must be a valid directory, - * but the path does not have to be absolute. - * @param lowerPrivilege Whether to lower the application's privileges. - * @param lowestUser The user to fallback to if lowering privilege fails. - * @param environment The RAILS_ENV/RACK_ENV environment that should be used. May not be empty. - * @param spawnMethod The spawn method to use. Either "smart" or "conservative". - * See the Ruby class SpawnManager for details. - * @param appType The application type. Either "rails" or "rack". + * @param PoolOptions An object containing the details for this spawn operation, + * such as which application to spawn. See PoolOptions for details. * @return A smart pointer to an Application object, which represents the application * instance that has been spawned. Use this object to communicate with the * spawned application. * @throws SpawnException Something went wrong. * @throws boost::thread_interrupted */ - ApplicationPtr spawn( - const string &appRoot, - bool lowerPrivilege = true, - const string &lowestUser = "nobody", - const string &environment = "production", - const string &spawnMethod = "smart", - const string &appType = "rails" - ) { - mutex::scoped_lock l(lock); + ApplicationPtr spawn(const PoolOptions &PoolOptions) { + TRACE_POINT(); + boost::mutex::scoped_lock l(lock); try { - return sendSpawnCommand(appRoot, lowerPrivilege, lowestUser, - environment, spawnMethod, appType); + return sendSpawnCommand(PoolOptions); } catch (const SpawnException &e) { if (e.hasErrorPage()) { throw; } else { - return handleSpawnException(e, appRoot, lowerPrivilege, - lowestUser, environment, spawnMethod, appType); + return handleSpawnException(e, PoolOptions); } } } @@ -544,6 +519,7 @@ class SpawnManager { * restart was attempted, but it failed. */ void reload(const string &appRoot) { + TRACE_POINT(); this_thread::disable_interruption di; this_thread::disable_syscall_interruption dsi; try { diff --git a/ext/apache2/StandardApplicationPool.h b/ext/apache2/StandardApplicationPool.h index e0c3f71e..08d1ee5f 100644 --- a/ext/apache2/StandardApplicationPool.h +++ b/ext/apache2/StandardApplicationPool.h @@ -27,6 +27,9 @@ #include #include +#include +#include + #include #include #include @@ -44,7 +47,8 @@ #include "ApplicationPool.h" #include "Logging.h" -#include "System.h" +#include "FileChecker.h" +#include "CachedFileStat.h" #ifdef PASSENGER_USE_DUMMY_SPAWN_MANAGER #include "DummySpawnManager.h" #else @@ -55,6 +59,7 @@ namespace Passenger { using namespace std; using namespace boost; +using namespace oxt; class ApplicationPoolServer; @@ -99,37 +104,100 @@ class StandardApplicationPool: public ApplicationPool { static const unsigned int GET_TIMEOUT = 5000; // In milliseconds. friend class ApplicationPoolServer; + struct Domain; struct AppContainer; + typedef shared_ptr DomainPtr; typedef shared_ptr AppContainerPtr; typedef list AppContainerList; - typedef shared_ptr AppContainerListPtr; - typedef map ApplicationMap; + typedef map DomainMap; + + struct Domain { + AppContainerList instances; + unsigned int size; + unsigned long maxRequests; + FileChecker restartFileChecker; + CachedFileStat alwaysRestartFileStatter; + + Domain(const PoolOptions &options) + : restartFileChecker(determineRestartDir(options) + "/restart.txt"), + alwaysRestartFileStatter(determineRestartDir(options) + "/always_restart.txt") + { + } + + private: + static string determineRestartDir(const PoolOptions &options) { + if (options.restartDir.empty()) { + return options.appRoot + "/tmp"; + } else if (options.restartDir[0] == '/') { + return options.restartDir; + } else { + return options.appRoot + "/" + options.restartDir; + } + } + }; struct AppContainer { ApplicationPtr app; + time_t startTime; time_t lastUsed; unsigned int sessions; + unsigned int processed; AppContainerList::iterator iterator; AppContainerList::iterator ia_iterator; + + AppContainer() { + startTime = time(NULL); + processed = 0; + } + + /** + * Returns the uptime of this AppContainer so far, as a string. + */ + string uptime() const { + time_t seconds = time(NULL) - startTime; + stringstream result; + + if (seconds >= 60) { + time_t minutes = seconds / 60; + if (minutes >= 60) { + time_t hours = minutes / 60; + minutes = minutes % 60; + result << hours << "h "; + } + + seconds = seconds % 60; + result << minutes << "m "; + } + result << seconds << "s"; + return result.str(); + } }; + /** + * A data structure which contains data that's shared between a + * StandardApplicationPool and a SessionCloseCallback object. + * This is because the StandardApplicationPool's life time could be + * different from a SessionCloseCallback's. + */ struct SharedData { - mutex lock; + boost::mutex lock; condition activeOrMaxChanged; - ApplicationMap apps; + DomainMap domains; unsigned int max; unsigned int count; unsigned int active; unsigned int maxPerApp; AppContainerList inactiveApps; - map restartFileTimes; map appInstanceCount; }; typedef shared_ptr SharedDataPtr; + /** + * Function object which will be called when a session has been closed. + */ struct SessionCloseCallback { SharedDataPtr data; weak_ptr container; @@ -141,28 +209,42 @@ class StandardApplicationPool: public ApplicationPool { } void operator()() { - mutex::scoped_lock l(data->lock); + boost::mutex::scoped_lock l(data->lock); AppContainerPtr container(this->container.lock()); if (container == NULL) { return; } - ApplicationMap::iterator it; - it = data->apps.find(container->app->getAppRoot()); - if (it != data->apps.end()) { - AppContainerListPtr list(it->second); - container->lastUsed = time(NULL); - container->sessions--; - if (container->sessions == 0) { - list->erase(container->iterator); - list->push_front(container); - container->iterator = list->begin(); - data->inactiveApps.push_back(container); - container->ia_iterator = data->inactiveApps.end(); - container->ia_iterator--; + DomainMap::iterator it; + it = data->domains.find(container->app->getAppRoot()); + if (it != data->domains.end()) { + Domain *domain = it->second.get(); + AppContainerList *instances = &domain->instances; + + container->processed++; + if (domain->maxRequests > 0 && container->processed >= domain->maxRequests) { + instances->erase(container->iterator); + domain->size--; + if (instances->empty()) { + data->domains.erase(container->app->getAppRoot()); + } + data->count--; data->active--; data->activeOrMaxChanged.notify_all(); + } else { + container->lastUsed = time(NULL); + container->sessions--; + if (container->sessions == 0) { + instances->erase(container->iterator); + instances->push_front(container); + container->iterator = instances->begin(); + data->inactiveApps.push_back(container); + container->ia_iterator = data->inactiveApps.end(); + container->ia_iterator--; + data->active--; + data->activeOrMaxChanged.notify_all(); + } } } } @@ -174,24 +256,22 @@ class StandardApplicationPool: public ApplicationPool { SpawnManager spawnManager; #endif SharedDataPtr data; - thread *cleanerThread; + boost::thread *cleanerThread; bool detached; bool done; - bool useGlobalQueue; unsigned int maxIdleTime; unsigned int waitingOnGlobalQueue; condition cleanerThreadSleeper; // Shortcuts for instance variables in SharedData. Saves typing in get(). - mutex &lock; + boost::mutex &lock; condition &activeOrMaxChanged; - ApplicationMap &apps; + DomainMap &domains; unsigned int &max; unsigned int &count; unsigned int &active; unsigned int &maxPerApp; AppContainerList &inactiveApps; - map &restartFileTimes; map &appInstanceCount; /** @@ -199,25 +279,38 @@ class StandardApplicationPool: public ApplicationPool { */ bool inline verifyState() { #if PASSENGER_DEBUG - // Invariant for _apps_. - ApplicationMap::const_iterator it; - for (it = apps.begin(); it != apps.end(); it++) { - AppContainerList *list = it->second.get(); - P_ASSERT(!list->empty(), false, "List for '" << it->first << "' is nonempty."); + // Invariants for _domains_. + DomainMap::const_iterator it; + unsigned int totalSize = 0; + for (it = domains.begin(); it != domains.end(); it++) { + const string &appRoot = it->first; + Domain *domain = it->second.get(); + AppContainerList *instances = &domain->instances; + + P_ASSERT(domain->size <= count, false, + "domains['" << appRoot << "'].size (" << domain->size << + ") <= count (" << count << ")"); + totalSize += domain->size; + + // Invariants for Domain. + + P_ASSERT(!instances->empty(), false, + "domains['" << appRoot << "'].instances is nonempty."); AppContainerList::const_iterator prev_lit; AppContainerList::const_iterator lit; - prev_lit = list->begin(); + prev_lit = instances->begin(); lit = prev_lit; lit++; - for (; lit != list->end(); lit++) { + for (; lit != instances->end(); lit++) { if ((*prev_lit)->sessions > 0) { P_ASSERT((*lit)->sessions > 0, false, - "List for '" << it->first << - "' is sorted from nonactive to active"); + "domains['" << appRoot << "'].instances " + "is sorted from nonactive to active"); } } } + P_ASSERT(totalSize == count, false, "(sum of all d.size in domains) == count"); P_ASSERT(active <= count, false, "active (" << active << ") < count (" << count << ")"); @@ -227,9 +320,7 @@ class StandardApplicationPool: public ApplicationPool { return true; } - template - string toString(LockActionType lockAction) const { - unique_lock l(lock, lockAction); + string toStringWithoutLock() const { stringstream result; result << "----------- General information -----------" << endl; @@ -237,23 +328,27 @@ class StandardApplicationPool: public ApplicationPool { result << "count = " << count << endl; result << "active = " << active << endl; result << "inactive = " << inactiveApps.size() << endl; - result << "Using global queue: " << (useGlobalQueue ? "yes" : "no") << endl; result << "Waiting on global queue: " << waitingOnGlobalQueue << endl; result << endl; - result << "----------- Applications -----------" << endl; - ApplicationMap::const_iterator it; - for (it = apps.begin(); it != apps.end(); it++) { - AppContainerList *list = it->second.get(); + result << "----------- Domains -----------" << endl; + DomainMap::const_iterator it; + for (it = domains.begin(); it != domains.end(); it++) { + Domain *domain = it->second.get(); + AppContainerList *instances = &domain->instances; AppContainerList::const_iterator lit; result << it->first << ": " << endl; - for (lit = list->begin(); lit != list->end(); lit++) { + for (lit = instances->begin(); lit != instances->end(); lit++) { AppContainer *container = lit->get(); char buf[128]; - snprintf(buf, sizeof(buf), "PID: %-8d Sessions: %d", - container->app->getPid(), container->sessions); + snprintf(buf, sizeof(buf), + "PID: %-5lu Sessions: %-2u Processed: %-5u Uptime: %s", + (unsigned long) container->app->getPid(), + container->sessions, + container->processed, + container->uptime().c_str()); result << " " << buf << endl; } result << endl; @@ -261,45 +356,14 @@ class StandardApplicationPool: public ApplicationPool { return result.str(); } - bool needsRestart(const string &appRoot) { - string restartFile(appRoot); - restartFile.append("/tmp/restart.txt"); - - struct stat buf; - bool result; - int ret; - - do { - ret = stat(restartFile.c_str(), &buf); - } while (ret == -1 && errno == EINTR); - if (ret == 0) { - do { - ret = unlink(restartFile.c_str()); - } while (ret == -1 && (errno == EINTR || errno == EAGAIN)); - if (ret == 0 || errno == ENOENT) { - restartFileTimes.erase(appRoot); - result = true; - } else { - map::const_iterator it; - - it = restartFileTimes.find(appRoot); - if (it == restartFileTimes.end()) { - result = true; - } else { - result = buf.st_mtime != restartFileTimes[appRoot]; - } - restartFileTimes[appRoot] = buf.st_mtime; - } - } else { - restartFileTimes.erase(appRoot); - result = false; - } - return result; + bool needsRestart(const string &appRoot, Domain *domain, const PoolOptions &options) { + return domain->alwaysRestartFileStatter.refresh(options.statThrottleRate) == 0 + || domain->restartFileChecker.changed(options.statThrottleRate); } void cleanerThreadMainLoop() { this_thread::disable_syscall_interruption dsi; - unique_lock l(lock); + unique_lock l(lock); try { while (!done && !this_thread::interruption_requested()) { xtime xt; @@ -316,31 +380,31 @@ class StandardApplicationPool: public ApplicationPool { } } - time_t now = InterruptableCalls::time(NULL); + time_t now = syscalls::time(NULL); AppContainerList::iterator it; for (it = inactiveApps.begin(); it != inactiveApps.end(); it++) { AppContainer &container(*it->get()); ApplicationPtr app(container.app); - AppContainerListPtr appList(apps[app->getAppRoot()]); + Domain *domain = domains[app->getAppRoot()].get(); + AppContainerList *instances = &domain->instances; - if (now - container.lastUsed > (time_t) maxIdleTime) { + if (maxIdleTime > 0 && + (now - container.lastUsed > (time_t) maxIdleTime)) { P_DEBUG("Cleaning idle app " << app->getAppRoot() << " (PID " << app->getPid() << ")"); - appList->erase(container.iterator); + instances->erase(container.iterator); AppContainerList::iterator prev = it; prev--; inactiveApps.erase(it); it = prev; - appInstanceCount[app->getAppRoot()]--; + domain->size--; count--; } - if (appList->empty()) { - apps.erase(app->getAppRoot()); - appInstanceCount.erase(app->getAppRoot()); - data->restartFileTimes.erase(app->getAppRoot()); + if (instances->empty()) { + domains.erase(app->getAppRoot()); } } } @@ -350,34 +414,31 @@ class StandardApplicationPool: public ApplicationPool { } /** + * Spawn a new application instance, or use an existing one that's in the pool. + * * @throws boost::thread_interrupted * @throws SpawnException * @throws SystemException */ - pair - spawnOrUseExisting( - mutex::scoped_lock &l, - const string &appRoot, - bool lowerPrivilege, - const string &lowestUser, - const string &environment, - const string &spawnMethod, - const string &appType - ) { + pair + spawnOrUseExisting(boost::mutex::scoped_lock &l, const PoolOptions &options) { beginning_of_function: + TRACE_POINT(); this_thread::disable_interruption di; this_thread::disable_syscall_interruption dsi; + const string &appRoot(options.appRoot); AppContainerPtr container; - AppContainerList *list; + Domain *domain; + AppContainerList *instances; try { - ApplicationMap::iterator it(apps.find(appRoot)); + DomainMap::iterator it(domains.find(appRoot)); - if (it != apps.end() && needsRestart(appRoot)) { + if (it != domains.end() && needsRestart(appRoot, it->second.get(), options)) { AppContainerList::iterator it2; - list = it->second.get(); - for (it2 = list->begin(); it2 != list->end(); it2++) { + instances = &it->second->instances; + for (it2 = instances->begin(); it2 != instances->end(); it2++) { container = *it2; if (container->sessions == 0) { inactiveApps.erase(container->ia_iterator); @@ -385,49 +446,50 @@ class StandardApplicationPool: public ApplicationPool { active--; } it2--; - list->erase(container->iterator); + instances->erase(container->iterator); count--; } - apps.erase(appRoot); - appInstanceCount.erase(appRoot); + domains.erase(appRoot); spawnManager.reload(appRoot); - it = apps.end(); + it = domains.end(); activeOrMaxChanged.notify_all(); } - if (it != apps.end()) { - list = it->second.get(); + if (it != domains.end()) { + domain = it->second.get(); + instances = &domain->instances; - if (list->front()->sessions == 0) { - container = list->front(); - list->pop_front(); - list->push_back(container); - container->iterator = list->end(); + if (instances->front()->sessions == 0) { + container = instances->front(); + instances->pop_front(); + instances->push_back(container); + container->iterator = instances->end(); container->iterator--; inactiveApps.erase(container->ia_iterator); active++; activeOrMaxChanged.notify_all(); } else if (count >= max || ( - maxPerApp != 0 && appInstanceCount[appRoot] >= maxPerApp ) + maxPerApp != 0 && domain->size >= maxPerApp ) ) { - if (useGlobalQueue) { + if (options.useGlobalQueue) { + UPDATE_TRACE_POINT(); waitingOnGlobalQueue++; activeOrMaxChanged.wait(l); waitingOnGlobalQueue--; goto beginning_of_function; } else { - AppContainerList::iterator it(list->begin()); - AppContainerList::iterator smallest(list->begin()); + AppContainerList::iterator it(instances->begin()); + AppContainerList::iterator smallest(instances->begin()); it++; - for (; it != list->end(); it++) { + for (; it != instances->end(); it++) { if ((*it)->sessions < (*smallest)->sessions) { smallest = it; } } container = *smallest; - list->erase(smallest); - list->push_back(container); - container->iterator = list->end(); + instances->erase(smallest); + instances->push_back(container); + container->iterator = instances->end(); container->iterator--; } } else { @@ -435,57 +497,57 @@ class StandardApplicationPool: public ApplicationPool { { this_thread::restore_interruption ri(di); this_thread::restore_syscall_interruption rsi(dsi); - container->app = spawnManager.spawn(appRoot, - lowerPrivilege, lowestUser, environment, - spawnMethod, appType); + container->app = spawnManager.spawn(options); } container->sessions = 0; - list->push_back(container); - container->iterator = list->end(); + instances->push_back(container); + container->iterator = instances->end(); container->iterator--; - appInstanceCount[appRoot]++; + domain->size++; count++; active++; activeOrMaxChanged.notify_all(); } } else { if (active >= max) { + UPDATE_TRACE_POINT(); activeOrMaxChanged.wait(l); goto beginning_of_function; - } - if (count == max) { + } else if (count == max) { container = inactiveApps.front(); inactiveApps.pop_front(); - list = apps[container->app->getAppRoot()].get(); - list->erase(container->iterator); - if (list->empty()) { - apps.erase(container->app->getAppRoot()); - restartFileTimes.erase(container->app->getAppRoot()); - appInstanceCount.erase(container->app->getAppRoot()); + domain = domains[container->app->getAppRoot()].get(); + instances = &domain->instances; + instances->erase(container->iterator); + if (instances->empty()) { + domains.erase(container->app->getAppRoot()); } else { - appInstanceCount[container->app->getAppRoot()]--; + domain->size--; } count--; } + + UPDATE_TRACE_POINT(); container = ptr(new AppContainer()); { this_thread::restore_interruption ri(di); this_thread::restore_syscall_interruption rsi(dsi); - container->app = spawnManager.spawn(appRoot, lowerPrivilege, lowestUser, - environment, spawnMethod, appType); + container->app = spawnManager.spawn(options); } container->sessions = 0; - it = apps.find(appRoot); - if (it == apps.end()) { - list = new AppContainerList(); - apps[appRoot] = ptr(list); - appInstanceCount[appRoot] = 1; + it = domains.find(appRoot); + if (it == domains.end()) { + domain = new Domain(options); + domain->size = 1; + domain->maxRequests = options.maxRequests; + domains[appRoot] = ptr(domain); } else { - list = it->second.get(); - appInstanceCount[appRoot]++; + domain = it->second.get(); + domain->size++; } - list->push_back(container); - container->iterator = list->end(); + instances = &domain->instances; + instances->push_back(container); + container->iterator = instances->end(); container->iterator--; count++; active++; @@ -509,7 +571,7 @@ class StandardApplicationPool: public ApplicationPool { throw SpawnException(message); } - return make_pair(container, list); + return make_pair(container, domain); } public: @@ -529,7 +591,6 @@ class StandardApplicationPool: public ApplicationPool { * running as root. If the empty string is given, or if * the user is not a valid username, then * the spawn manager will be run as the current user. - * @param rubyCommand The Ruby interpreter's command. * @throws SystemException An error occured while trying to setup the spawn server. * @throws IOException The specified log file could not be opened. */ @@ -544,25 +605,24 @@ class StandardApplicationPool: public ApplicationPool { data(new SharedData()), lock(data->lock), activeOrMaxChanged(data->activeOrMaxChanged), - apps(data->apps), + domains(data->domains), max(data->max), count(data->count), active(data->active), maxPerApp(data->maxPerApp), inactiveApps(data->inactiveApps), - restartFileTimes(data->restartFileTimes), appInstanceCount(data->appInstanceCount) { + TRACE_POINT(); detached = false; done = false; max = DEFAULT_MAX_POOL_SIZE; count = 0; active = 0; - useGlobalQueue = false; waitingOnGlobalQueue = 0; maxPerApp = DEFAULT_MAX_INSTANCES_PER_APP; maxIdleTime = DEFAULT_MAX_IDLE_TIME; - cleanerThread = new thread( + cleanerThread = new boost::thread( bind(&StandardApplicationPool::cleanerThreadMainLoop, this), CLEANER_THREAD_STACK_SIZE ); @@ -572,7 +632,7 @@ class StandardApplicationPool: public ApplicationPool { if (!detached) { this_thread::disable_interruption di; { - mutex::scoped_lock l(lock); + boost::mutex::scoped_lock l(lock); done = true; cleanerThreadSleeper.notify_one(); } @@ -581,28 +641,27 @@ class StandardApplicationPool: public ApplicationPool { delete cleanerThread; } - virtual Application::SessionPtr get( - const string &appRoot, - bool lowerPrivilege = true, - const string &lowestUser = "nobody", - const string &environment = "production", - const string &spawnMethod = "smart", - const string &appType = "rails" - ) { + virtual Application::SessionPtr get(const string &appRoot) { + return ApplicationPool::get(appRoot); + } + + virtual Application::SessionPtr get(const PoolOptions &options) { + TRACE_POINT(); using namespace boost::posix_time; unsigned int attempt = 0; - ptime timeLimit(get_system_time() + millisec(GET_TIMEOUT)); - unique_lock l(lock); + // TODO: We should probably add a timeout to the following + // lock. This way we can fail gracefully if the server's under + // rediculous load. Though I'm not sure how much it really helps. + unique_lock l(lock); while (true) { attempt++; - pair p( - spawnOrUseExisting(l, appRoot, lowerPrivilege, lowestUser, - environment, spawnMethod, appType) + pair p( + spawnOrUseExisting(l, options) ); - AppContainerPtr &container(p.first); - AppContainerList &list(*p.second); + AppContainerPtr &container = p.first; + Domain *domain = p.second; container->lastUsed = time(NULL); container->sessions++; @@ -610,13 +669,26 @@ class StandardApplicationPool: public ApplicationPool { P_ASSERT(verifyState(), Application::SessionPtr(), "State is valid:\n" << toString(false)); try { + UPDATE_TRACE_POINT(); return container->app->connect(SessionCloseCallback(data, container)); } catch (const exception &e) { container->sessions--; + + AppContainerList &instances(domain->instances); + instances.erase(container->iterator); + domain->size--; + if (instances.empty()) { + domains.erase(options.appRoot); + } + count--; + active--; + activeOrMaxChanged.notify_all(); + P_ASSERT(verifyState(), Application::SessionPtr(), + "State is valid: " << toString(false)); if (attempt == MAX_GET_ATTEMPTS) { string message("Cannot connect to an existing " "application instance for '"); - message.append(appRoot); + message.append(options.appRoot); message.append("': "); try { const SystemException &syse = @@ -626,17 +698,6 @@ class StandardApplicationPool: public ApplicationPool { message.append(e.what()); } throw IOException(message); - } else { - list.erase(container->iterator); - if (list.empty()) { - apps.erase(appRoot); - appInstanceCount.erase(appRoot); - } - count--; - active--; - activeOrMaxChanged.notify_all(); - P_ASSERT(verifyState(), Application::SessionPtr(), - "State is valid."); } } } @@ -645,10 +706,9 @@ class StandardApplicationPool: public ApplicationPool { } virtual void clear() { - mutex::scoped_lock l(lock); - apps.clear(); + boost::mutex::scoped_lock l(lock); + domains.clear(); inactiveApps.clear(); - restartFileTimes.clear(); appInstanceCount.clear(); count = 0; active = 0; @@ -656,13 +716,13 @@ class StandardApplicationPool: public ApplicationPool { } virtual void setMaxIdleTime(unsigned int seconds) { - mutex::scoped_lock l(lock); + boost::mutex::scoped_lock l(lock); maxIdleTime = seconds; cleanerThreadSleeper.notify_one(); } virtual void setMax(unsigned int max) { - mutex::scoped_lock l(lock); + boost::mutex::scoped_lock l(lock); this->max = max; activeOrMaxChanged.notify_all(); } @@ -676,15 +736,11 @@ class StandardApplicationPool: public ApplicationPool { } virtual void setMaxPerApp(unsigned int maxPerApp) { - mutex::scoped_lock l(lock); + boost::mutex::scoped_lock l(lock); this->maxPerApp = maxPerApp; activeOrMaxChanged.notify_all(); } - virtual void setUseGlobalQueue(bool value) { - this->useGlobalQueue = value; - } - virtual pid_t getSpawnServerPid() const { return spawnManager.getServerPid(); } @@ -695,13 +751,58 @@ class StandardApplicationPool: public ApplicationPool { */ virtual string toString(bool lockMutex = true) const { if (lockMutex) { - return toString(boost::adopt_lock); + unique_lock l(lock); + return toStringWithoutLock(); } else { - return toString(boost::defer_lock); + return toStringWithoutLock(); } } + + /** + * Returns an XML description of the internal state of the + * application pool. + */ + virtual string toXml() const { + unique_lock l(lock); + stringstream result; + DomainMap::const_iterator it; + + result << "\n"; + result << ""; + + result << ""; + for (it = domains.begin(); it != domains.end(); it++) { + Domain *domain = it->second.get(); + AppContainerList *instances = &domain->instances; + AppContainerList::const_iterator lit; + + result << ""; + result << "" << escapeForXml(it->first) << ""; + + result << ""; + for (lit = instances->begin(); lit != instances->end(); lit++) { + AppContainer *container = lit->get(); + + result << ""; + result << "" << container->app->getPid() << ""; + result << "" << container->sessions << ""; + result << "" << container->processed << ""; + result << "" << container->uptime() << ""; + result << ""; + } + result << ""; + + result << ""; + } + result << ""; + + result << ""; + return result.str(); + } }; +typedef shared_ptr StandardApplicationPoolPtr; + } // namespace Passenger #endif /* _PASSENGER_STANDARD_APPLICATION_POOL_H_ */ diff --git a/ext/apache2/System.h b/ext/apache2/System.h deleted file mode 100644 index e5f6999f..00000000 --- a/ext/apache2/System.h +++ /dev/null @@ -1,228 +0,0 @@ -/* - * Phusion Passenger - http://www.modrails.com/ - * Copyright (C) 2008 Phusion - * - * Phusion Passenger is a trademark of Hongli Lai & Ninh Bui. - * - * 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. - */ -#ifndef _PASSENGER_SYSTEM_H_ -#define _PASSENGER_SYSTEM_H_ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -/** - * Support for interruption of blocking system calls and C library calls. - * - * This file provides a framework for writing multithreading code that can - * be interrupted, even when blocked on system calls or C library calls. - * - * One must first call Passenger::setupSysCallInterruptionSupport(). - * Then one may use the functions in Passenger::InterruptableCalls - * as drop-in replacements for system calls or C library functions. - * Thread::interrupt() and Thread::interruptAndJoin() should be used - * for interrupting threads. - * - * By default, interruptions are caught. - */ - -// This is one of the things that Java is good at and C++ sucks at. Sigh... - -namespace Passenger { - - using namespace boost; - - static const int INTERRUPTION_SIGNAL = SIGINT; - - /** - * Setup system call interruption support. - * This function may only be called once. It installs a signal handler - * for INTERRUPTION_SIGNAL, so one should not install a different signal - * handler for that signal after calling this function. - */ - void setupSyscallInterruptionSupport(); - - /** - * Thread class with system call interruption support. - */ - class Thread: public thread { - public: - template - explicit Thread(F f, unsigned int stackSize = 0) - : thread(f, stackSize) {} - - /** - * Interrupt the thread. This method behaves just like - * boost::thread::interrupt(), but will also respect the interruption - * points defined in Passenger::InterruptableCalls. - * - * Note that an interruption request may get lost, depending on the - * current execution point of the thread. Thus, one should call this - * method in a loop, until a certain goal condition has been fulfilled. - * interruptAndJoin() is a convenience method that implements this - * pattern. - */ - void interrupt() { - int ret; - - thread::interrupt(); - do { - ret = pthread_kill(native_handle(), - INTERRUPTION_SIGNAL); - } while (ret == EINTR); - } - - /** - * Keep interrupting the thread until it's done, then join it. - * - * @throws boost::thread_interrupted - */ - void interruptAndJoin() { - bool done = false; - while (!done) { - interrupt(); - done = timed_join(posix_time::millisec(10)); - } - } - }; - - /** - * System call and C library call wrappers with interruption support. - * These functions are interruption points, i.e. they throw boost::thread_interrupted - * whenever the calling thread is interrupted by Thread::interrupt() or - * Thread::interruptAndJoin(). - */ - namespace InterruptableCalls { - ssize_t read(int fd, void *buf, size_t count); - ssize_t write(int fd, const void *buf, size_t count); - int close(int fd); - - int socketpair(int d, int type, int protocol, int sv[2]); - ssize_t recvmsg(int s, struct msghdr *msg, int flags); - ssize_t sendmsg(int s, const struct msghdr *msg, int flags); - int shutdown(int s, int how); - - FILE *fopen(const char *path, const char *mode); - int fclose(FILE *fp); - - time_t time(time_t *t); - int usleep(useconds_t usec); - int nanosleep(const struct timespec *req, struct timespec *rem); - - pid_t fork(); - int kill(pid_t pid, int sig); - pid_t waitpid(pid_t pid, int *status, int options); - } - -} // namespace Passenger - -namespace boost { -namespace this_thread { - - /** - * @intern - */ - extern thread_specific_ptr _syscalls_interruptable; - - /** - * Check whether system calls should be interruptable in - * the calling thread. - */ - bool syscalls_interruptable(); - - class restore_syscall_interruption; - - /** - * Create this struct on the stack to temporarily enable system - * call interruption, until the object goes out of scope. - */ - class enable_syscall_interruption { - private: - bool lastValue; - public: - enable_syscall_interruption() { - if (_syscalls_interruptable.get() == NULL) { - lastValue = true; - _syscalls_interruptable.reset(new bool(true)); - } else { - lastValue = *_syscalls_interruptable; - *_syscalls_interruptable = true; - } - } - - ~enable_syscall_interruption() { - *_syscalls_interruptable = lastValue; - } - }; - - /** - * Create this struct on the stack to temporarily disable system - * call interruption, until the object goes out of scope. - * While system call interruption is disabled, the functions in - * InterruptableCalls will try until the return code is not EINTR. - */ - class disable_syscall_interruption { - private: - friend class restore_syscall_interruption; - bool lastValue; - public: - disable_syscall_interruption() { - if (_syscalls_interruptable.get() == NULL) { - lastValue = true; - _syscalls_interruptable.reset(new bool(false)); - } else { - lastValue = *_syscalls_interruptable; - *_syscalls_interruptable = false; - } - } - - ~disable_syscall_interruption() { - *_syscalls_interruptable = lastValue; - } - }; - - /** - * Creating an object of this class on the stack will restore the - * system call interruption state to what it was before. - */ - class restore_syscall_interruption { - private: - int lastValue; - public: - restore_syscall_interruption(const disable_syscall_interruption &intr) { - assert(_syscalls_interruptable.get() != NULL); - lastValue = *_syscalls_interruptable; - *_syscalls_interruptable = intr.lastValue; - } - - ~restore_syscall_interruption() { - *_syscalls_interruptable = lastValue; - } - }; - -} // namespace this_thread -} // namespace boost - -#endif /* _PASSENGER_SYSTEM_H_ */ - diff --git a/ext/apache2/SystemTime.cpp b/ext/apache2/SystemTime.cpp new file mode 100644 index 00000000..24110696 --- /dev/null +++ b/ext/apache2/SystemTime.cpp @@ -0,0 +1,28 @@ +/* + * Phusion Passenger - http://www.modrails.com/ + * Copyright (C) 2009 Phusion + * + * Phusion Passenger is a trademark of Hongli Lai & Ninh Bui. + * + * 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. + */ + +#include + +namespace Passenger { + namespace SystemTimeData { + bool hasForcedValue = false; + time_t forcedValue = 0; + } +} diff --git a/ext/apache2/SystemTime.h b/ext/apache2/SystemTime.h new file mode 100644 index 00000000..74010150 --- /dev/null +++ b/ext/apache2/SystemTime.h @@ -0,0 +1,82 @@ +/* + * Phusion Passenger - http://www.modrails.com/ + * Copyright (C) 2009 Phusion + * + * Phusion Passenger is a trademark of Hongli Lai & Ninh Bui. + * + * 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. + */ +#ifndef _PASSENGER_SYSTEM_TIME_H_ +#define _PASSENGER_SYSTEM_TIME_H_ + +#include +#include +#include "Exceptions.h" + +namespace Passenger { + +using namespace oxt; + +namespace SystemTimeData { + extern bool hasForcedValue; + extern time_t forcedValue; +} + +/** + * This class allows one to obtain the system time, similar to time(). Unlike + * time(), it is possible to force a certain time to be returned, which is + * useful for testing code that depends on the system time. + */ +class SystemTime { +public: + /** + * Returns the time since the Epoch, measured in seconds. Or, if a time + * was forced, then the forced time is returned instead. + * + * @throws SystemException Something went wrong while retrieving the time. + * @throws boost::thread_interrupted + */ + static time_t get() { + if (SystemTimeData::hasForcedValue) { + return SystemTimeData::forcedValue; + } else { + time_t ret = syscalls::time(NULL); + if (ret == -1) { + throw SystemException("Unable to retrieve the system time", + errno); + } + return ret; + } + } + + /** + * Force get() to return the given value. + */ + static void force(time_t value) { + SystemTimeData::hasForcedValue = true; + SystemTimeData::forcedValue = value; + } + + /** + * Release the previously forced value, so that get() + * returns the system time once again. + */ + static void release() { + SystemTimeData::hasForcedValue = false; + } +}; + +} // namespace Passenger + +#endif /* _PASSENGER_SYSTEM_TIME_H_ */ diff --git a/ext/apache2/Utils.cpp b/ext/apache2/Utils.cpp index 2e53e807..a1027d7e 100644 --- a/ext/apache2/Utils.cpp +++ b/ext/apache2/Utils.cpp @@ -19,7 +19,7 @@ */ #include -#include +#include "CachedFileStat.h" #include "Utils.h" #define SPAWN_SERVER_SCRIPT_NAME "passenger-spawn-server" @@ -31,6 +31,11 @@ atoi(const string &s) { return ::atoi(s.c_str()); } +long +atol(const string &s) { + return ::atol(s.c_str()); +} + void split(const string &str, char sep, vector &output) { string::size_type start, pos; @@ -44,14 +49,31 @@ split(const string &str, char sep, vector &output) { } bool -fileExists(const char *filename) { +fileExists(const char *filename, CachedMultiFileStat *mstat, unsigned int throttleRate) { + return getFileType(filename, mstat, throttleRate) == FT_REGULAR; +} + +FileType +getFileType(const char *filename, CachedMultiFileStat *mstat, unsigned int throttleRate) { struct stat buf; + int ret; - if (stat(filename, &buf) == 0) { - return S_ISREG(buf.st_mode); + if (mstat != NULL) { + ret = cached_multi_file_stat_perform(mstat, filename, &buf, throttleRate); + } else { + ret = stat(filename, &buf); + } + if (ret == 0) { + if (S_ISREG(buf.st_mode)) { + return FT_REGULAR; + } else if (S_ISDIR(buf.st_mode)) { + return FT_DIRECTORY; + } else { + return FT_OTHER; + } } else { if (errno == ENOENT) { - return false; + return FT_NONEXISTANT; } else { int e = errno; string message("Cannot stat '"); @@ -76,7 +98,7 @@ findSpawnServer(const char *passengerRoot) { return path; } else { path.assign(root); - path.append("lib/passenger/passenger-spawn-server"); + path.append("lib/phusion_passenger/passenger-spawn-server"); return path; } return path; @@ -115,7 +137,7 @@ findApplicationPoolServer(const char *passengerRoot) { return path; } else { path.assign(root); - path.append("lib/passenger/ApplicationPoolServerExecutable"); + path.append("lib/phusion_passenger/ApplicationPoolServerExecutable"); return path; } } @@ -128,7 +150,13 @@ canonicalizePath(const string &path) { // rationale. char *tmp = realpath(path.c_str(), NULL); if (tmp == NULL) { - return ""; + int e = errno; + string message; + + message = "Cannot resolve the path '"; + message.append(path); + message.append("'"); + throw FileSystemException(message, e, path); } else { string result(tmp); free(tmp); @@ -137,32 +165,158 @@ canonicalizePath(const string &path) { #else char tmp[PATH_MAX]; if (realpath(path.c_str(), tmp) == NULL) { - return ""; + int e = errno; + string message; + + message = "Cannot resolve the path '"; + message.append(path); + message.append("'"); + throw FileSystemException(message, e, path); } else { return tmp; } #endif } +string +escapeForXml(const string &input) { + string result(input); + string::size_type input_pos = 0; + string::size_type input_end_pos = input.size(); + string::size_type result_pos = 0; + + while (input_pos < input_end_pos) { + const unsigned char ch = input[input_pos]; + + if ((ch >= 'A' && ch <= 'z') + || (ch >= '0' && ch <= '9') + || ch == '/' || ch == ' ' || ch == '_' || ch == '.') { + // This is an ASCII character. Ignore it and + // go to next character. + result_pos++; + } else { + // Not an ASCII character; escape it. + char escapedCharacter[sizeof("ÿ") + 1]; + int size; + + size = snprintf(escapedCharacter, + sizeof(escapedCharacter) - 1, + "&#%d;", + (int) ch); + if (size < 0) { + throw std::bad_alloc(); + } + escapedCharacter[sizeof(escapedCharacter) - 1] = '\0'; + + result.replace(result_pos, 1, escapedCharacter, size); + result_pos += size; + } + input_pos++; + } + + return result; +} + +const char * +getTempDir() { + const char *temp_dir = getenv("TMPDIR"); + if (temp_dir == NULL || *temp_dir == '\0') { + temp_dir = "/tmp"; + } + return temp_dir; +} + +string +getPassengerTempDir(bool bypassCache) { + if (bypassCache) { + goto calculateResult; + } else { + const char *tmp = getenv("PHUSION_PASSENGER_TMP"); + if (tmp != NULL && *tmp != '\0') { + return tmp; + } else { + goto calculateResult; + } + } + + calculateResult: + const char *temp_dir = getTempDir(); + char buffer[PATH_MAX]; + + snprintf(buffer, sizeof(buffer), "%s/passenger.%lu", + temp_dir, (unsigned long) getpid()); + buffer[sizeof(buffer) - 1] = '\0'; + setenv("PHUSION_PASSENGER_TMP", buffer, 1); + return buffer; +} + +void +createPassengerTempDir() { + makeDirTree(getPassengerTempDir().c_str(), "u=rwxs,g=wx,o=wx"); +} + +void +makeDirTree(const char *path, const char *mode) { + char command[PATH_MAX + 10]; + snprintf(command, sizeof(command), "mkdir -p -m \"%s\" \"%s\"", mode, path); + command[sizeof(command) - 1] = '\0'; + + int result; + do { + result = system(command); + } while (result == -1 && errno == EINTR); + if (result != 0) { + char message[1024]; + int e = errno; + + snprintf(message, sizeof(message) - 1, "Cannot create directory '%s'", path); + message[sizeof(message) - 1] = '\0'; + if (result == -1) { + throw SystemException(message, e); + } else { + throw IOException(message); + } + } +} + +void +removeDirTree(const char *path) { + char command[PATH_MAX + 10]; + snprintf(command, sizeof(command), "rm -rf \"%s\"", path); + command[sizeof(command) - 1] = '\0'; + + int result; + do { + result = system(command); + } while (result == -1 && errno == EINTR); + if (result == -1) { + char message[1024]; + + snprintf(message, sizeof(message) - 1, "Cannot create directory '%s'", path); + message[sizeof(message) - 1] = '\0'; + throw IOException(message); + } +} + bool -verifyRailsDir(const string &dir) { +verifyRailsDir(const string &dir, CachedMultiFileStat *mstat, unsigned int throttleRate) { string temp(dir); - temp.append("/../config/environment.rb"); - return fileExists(temp.c_str()); + temp.append("/config/environment.rb"); + return fileExists(temp.c_str(), mstat, throttleRate); } bool -verifyRackDir(const string &dir) { +verifyRackDir(const string &dir, CachedMultiFileStat *mstat, unsigned int throttleRate) { string temp(dir); - temp.append("/../config.ru"); - return fileExists(temp.c_str()); + temp.append("/config.ru"); + return fileExists(temp.c_str(), mstat, throttleRate); } bool -verifyWSGIDir(const string &dir) { +verifyWSGIDir(const string &dir, CachedMultiFileStat *mstat, unsigned int throttleRate) { string temp(dir); - temp.append("/../passenger_wsgi.py"); - return fileExists(temp.c_str()); + temp.append("/passenger_wsgi.py"); + return fileExists(temp.c_str(), mstat, throttleRate); } } // namespace Passenger diff --git a/ext/apache2/Utils.h b/ext/apache2/Utils.h index 7579b7cc..bb3be2a8 100644 --- a/ext/apache2/Utils.h +++ b/ext/apache2/Utils.h @@ -32,13 +32,28 @@ #include #include #include +#include #include "Exceptions.h" +typedef struct CachedMultiFileStat CachedMultiFileStat; + namespace Passenger { using namespace std; using namespace boost; +/** Enumeration which indicates what kind of file a file is. */ +typedef enum { + /** The file doesn't exist. */ + FT_NONEXISTANT, + /** A regular file or a symlink to a regular file. */ + FT_REGULAR, + /** A directory. */ + FT_DIRECTORY, + /** Something else, e.g. a pipe or a socket. */ + FT_OTHER +} FileType; + /** * Convenience shortcut for creating a shared_ptr. * Instead of: @@ -118,6 +133,12 @@ toString(T something) { */ int atoi(const string &s); +/** + * Converts the given string to a long integer. + * @ingroup Support + */ +long atol(const string &s); + /** * Split the given string using the given separator. * @@ -132,11 +153,27 @@ void split(const string &str, char sep, vector &output); * Check whether the specified file exists. * * @param filename The filename to check. + * @param mstat A CachedMultiFileStat object, if you want to use cached statting. + * @param throttleRate A throttle rate for mstat. Only applicable if mstat is not NULL. * @return Whether the file exists. * @throws FileSystemException Unable to check because of a filesystem error. * @ingroup Support */ -bool fileExists(const char *filename); +bool fileExists(const char *filename, CachedMultiFileStat *mstat = 0, + unsigned int throttleRate = 0); + +/** + * Check whether 'filename' exists and what kind of file it is. + * + * @param filename The filename to check. + * @param mstat A CachedMultiFileStat object, if you want to use cached statting. + * @param throttleRate A throttle rate for mstat. Only applicable if mstat is not NULL. + * @return The file type. + * @throws FileSystemException Unable to check because of a filesystem error. + * @ingroup Support + */ +FileType getFileType(const char *filename, CachedMultiFileStat *mstat = 0, + unsigned int throttleRate = 0); /** * Find the location of the Passenger spawn server script. @@ -167,38 +204,109 @@ string findApplicationPoolServer(const char *passengerRoot); /** * Returns a canonical version of the specified path. All symbolic links * and relative path elements are resolved. - * Returns an empty string if something went wrong. * + * @throws FileSystemException Something went wrong. * @ingroup Support */ string canonicalizePath(const string &path); +/** + * Escape the given raw string into an XML value. + * + * @throws std::bad_alloc Something went wrong. + * @ingroup Support + */ +string escapeForXml(const string &input); + +/** + * Return the path name for the directory in which temporary files are + * to be stored. + * + * @ensure result != NULL + * @ingroup Support + */ +const char *getTempDir(); + +/** + * Return the path name for the directory in which Phusion Passenger-specific + * temporary files are to be stored. This directory is unique for this instance + * of the web server in which Phusion Passenger is running. + * + * The result will be cached into the PHUSION_PASSENGER_TMP environment variable, + * which will be used for future calls to this function. To bypass this cache, + * set 'bypassCache' to true. + * + * @ensure !result.empty() + * @ingroup Support + */ +string getPassengerTempDir(bool bypassCache = false); + +/* Create a temp folder for storing Phusion Passenger-specific temp files, + * such as temporarily buffered uploads, sockets for backend processes, etc. + * This call also sets the PHUSION_PASSENGER_TMP environment variable, which + * allows backend processes to find this temp folder. + * + * Does nothing if this folder already exists. + * + * @throws IOException Something went wrong. + * @throws SystemException Something went wrong. + */ +void createPassengerTempDir(); + +/** + * Create the directory at the given path, creating intermediate directories + * if necessary. The created directories' permissions are as specified by the + * 'mode' parameter. + * + * If 'path' already exists, then nothing will happen. + * + * @throws IOException Something went wrong. + * @throws SystemException Something went wrong. + */ +void makeDirTree(const char *path, const char *mode = "u=rwx,g=,o="); + +/** + * Remove an entire directory tree recursively. + * + * @throws SystemException Something went wrong. + */ +void removeDirTree(const char *path); + /** * Check whether the specified directory is a valid Ruby on Rails - * 'public' directory. + * application root directory. * + * @param mstat A CachedMultiFileStat object, if you want to use cached statting. + * @param throttleRate A throttle rate for mstat. Only applicable if mstat is not NULL. * @throws FileSystemException Unable to check because of a system error. * @ingroup Support */ -bool verifyRailsDir(const string &dir); +bool verifyRailsDir(const string &dir, CachedMultiFileStat *mstat = 0, + unsigned int throttleRate = 0); /** - * Check whether the specified directory is a valid Rack 'public' - * directory. + * Check whether the specified directory is a valid Rack application + * root directory. * + * @param mstat A CachedMultiFileStat object, if you want to use cached statting. + * @param throttleRate A throttle rate for mstat. Only applicable if mstat is not NULL. * @throws FileSystemException Unable to check because of a filesystem error. * @ingroup Support */ -bool verifyRackDir(const string &dir); +bool verifyRackDir(const string &dir, CachedMultiFileStat *mstat = 0, + unsigned int throttleRate = 0); /** - * Check whether the specified directory is a valid WSGI 'public' - * directory. + * Check whether the specified directory is a valid WSGI application + * root directory. * + * @param mstat A CachedMultiFileStat object, if you want to use cached statting. + * @param throttleRate A throttle rate for mstat. Only applicable if mstat is not NULL. * @throws FileSystemException Unable to check because of a filesystem error. * @ingroup Support */ -bool verifyWSGIDir(const string &dir); +bool verifyWSGIDir(const string &dir, CachedMultiFileStat *mstat = 0, + unsigned int throttleRate = 0); /** * Represents a temporary file. The associated file is automatically @@ -221,21 +329,18 @@ class TempFile { * a big not-in-memory buffer to work with. * @throws SystemException Something went wrong. */ - TempFile(bool anonymous = true) { - const char *temp_dir; + TempFile(const char *identifier = "temp", bool anonymous = true) { char templ[PATH_MAX]; int fd; - temp_dir = getenv("TMP"); - if (temp_dir == NULL || *temp_dir == '\0') { - temp_dir = "/tmp"; - } - - snprintf(templ, sizeof(templ), "%s/passenger.XXXXXX", temp_dir); + snprintf(templ, sizeof(templ), "%s/%s.XXXXXX", getPassengerTempDir().c_str(), identifier); templ[sizeof(templ) - 1] = '\0'; fd = mkstemp(templ); if (fd == -1) { - throw SystemException("Cannot create a temporary file", errno); + char message[1024]; + snprintf(message, sizeof(message), "Cannot create a temporary file '%s'", templ); + message[sizeof(message) - 1] = '\0'; + throw SystemException(message, errno); } if (anonymous) { fchmod(fd, 0000); diff --git a/ext/boost/cstdint.hpp b/ext/boost/cstdint.hpp index 31a432a8..09254cf0 100644 --- a/ext/boost/cstdint.hpp +++ b/ext/boost/cstdint.hpp @@ -122,8 +122,10 @@ namespace boost } // namespace boost -#elif defined(__FreeBSD__) && (__FreeBSD__ <= 4) || defined(__osf__) -// FreeBSD and Tru64 have an that contains much of what we need. +#elif defined(__FreeBSD__) && (__FreeBSD__ <= 4) || defined(__osf__) || \ + defined(__SOLARIS9__) +// FreeBSD, Tru64 and Solaris 9 have an that contains much of +// what we need. # include namespace boost { diff --git a/ext/boost/current_function.hpp b/ext/boost/current_function.hpp new file mode 100644 index 00000000..aa5756e0 --- /dev/null +++ b/ext/boost/current_function.hpp @@ -0,0 +1,67 @@ +#ifndef BOOST_CURRENT_FUNCTION_HPP_INCLUDED +#define BOOST_CURRENT_FUNCTION_HPP_INCLUDED + +// MS compatible compilers support #pragma once + +#if defined(_MSC_VER) && (_MSC_VER >= 1020) +# pragma once +#endif + +// +// boost/current_function.hpp - BOOST_CURRENT_FUNCTION +// +// Copyright (c) 2002 Peter Dimov and Multi Media Ltd. +// +// Distributed under the Boost Software License, Version 1.0. (See +// accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) +// +// http://www.boost.org/libs/utility/current_function.html +// + +namespace boost +{ + +namespace detail +{ + +inline void current_function_helper() +{ + +#if defined(__GNUC__) || (defined(__MWERKS__) && (__MWERKS__ >= 0x3000)) || (defined(__ICC) && (__ICC >= 600)) + +# define BOOST_CURRENT_FUNCTION __PRETTY_FUNCTION__ + +#elif defined(__DMC__) && (__DMC__ >= 0x810) + +# define BOOST_CURRENT_FUNCTION __PRETTY_FUNCTION__ + +#elif defined(__FUNCSIG__) + +# define BOOST_CURRENT_FUNCTION __FUNCSIG__ + +#elif (defined(__INTEL_COMPILER) && (__INTEL_COMPILER >= 600)) || (defined(__IBMCPP__) && (__IBMCPP__ >= 500)) + +# define BOOST_CURRENT_FUNCTION __FUNCTION__ + +#elif defined(__BORLANDC__) && (__BORLANDC__ >= 0x550) + +# define BOOST_CURRENT_FUNCTION __FUNC__ + +#elif defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901) + +# define BOOST_CURRENT_FUNCTION __func__ + +#else + +# define BOOST_CURRENT_FUNCTION "(unknown)" + +#endif + +} + +} // namespace detail + +} // namespace boost + +#endif // #ifndef BOOST_CURRENT_FUNCTION_HPP_INCLUDED diff --git a/ext/boost/detail/sp_counted_base.hpp b/ext/boost/detail/sp_counted_base.hpp index f925a5d5..dd16fb01 100644 --- a/ext/boost/detail/sp_counted_base.hpp +++ b/ext/boost/detail/sp_counted_base.hpp @@ -51,14 +51,14 @@ # include -#elif defined(__GNUC__) && ( __GNUC__ * 100 + __GNUC_MINOR__ >= 401 ) - -# include - #elif defined(__GNUC__) && ( defined( __sparcv8 ) || defined( __sparcv9 ) ) # include +#elif defined(__GNUC__) && ( __GNUC__ * 100 + __GNUC_MINOR__ >= 401 ) + +# include + #elif defined( WIN32 ) || defined( _WIN32 ) || defined( __WIN32__ ) # include diff --git a/ext/boost/thread/exceptions.hpp b/ext/boost/thread/exceptions.hpp index e712876d..68ae9168 100644 --- a/ext/boost/thread/exceptions.hpp +++ b/ext/boost/thread/exceptions.hpp @@ -9,6 +9,7 @@ #define BOOST_THREAD_EXCEPTIONS_PDM070801_H #include +#include // pdm: Sorry, but this class is used all over the place & I end up // with recursive headers if I don't separate it @@ -21,7 +22,7 @@ namespace boost { -class BOOST_THREAD_DECL thread_exception : public std::exception +class BOOST_THREAD_DECL thread_exception : public oxt::tracable_exception { protected: std::string message; diff --git a/ext/boost/thread/pthread/thread.hpp b/ext/boost/thread/pthread/thread.hpp index fc4fc47a..7fe69e01 100644 --- a/ext/boost/thread/pthread/thread.hpp +++ b/ext/boost/thread/pthread/thread.hpp @@ -134,15 +134,22 @@ namespace boost mutable boost::mutex thread_info_mutex; detail::thread_data_ptr thread_info; - void start_thread(unsigned int stack_size = 0); - explicit thread(detail::thread_data_ptr data); detail::thread_data_ptr get_thread_info() const; + protected: + template + void set_thread_main_function(F f) + { + thread_info = detail::thread_data_ptr(new thread_data(f)); + } + + void start_thread(unsigned int stack_size); + public: thread(); - ~thread(); + virtual ~thread(); template explicit thread(F f, unsigned int stack_size = 0): @@ -150,6 +157,7 @@ namespace boost { start_thread(stack_size); } + template thread(detail::thread_move_t f, unsigned int stack_size = 0): thread_info(new thread_data(f)) diff --git a/ext/boost/thread/pthread/thread_data.hpp b/ext/boost/thread/pthread/thread_data.hpp index 52ca40e5..c726f8f9 100644 --- a/ext/boost/thread/pthread/thread_data.hpp +++ b/ext/boost/thread/pthread/thread_data.hpp @@ -10,12 +10,13 @@ #include #include #include +#include #include #include "condition_variable_fwd.hpp" namespace boost { - class thread_interrupted + class thread_interrupted: public oxt::tracable_exception {}; namespace detail diff --git a/ext/oxt/Readme.txt b/ext/oxt/Readme.txt new file mode 100644 index 00000000..5e6277cb --- /dev/null +++ b/ext/oxt/Readme.txt @@ -0,0 +1,15 @@ +== Introduction + +OXT, OS eXtensions for boosT, is a utility library that provides important +functionality that's necessary for writing robust server software. It +provides essential things that should be part of C++, but unfortunately isn't, +such as: +- System call interruption support. This is important for multithreaded + software that can block on system calls. +- Support for backtraces. + +== Compilation and usage + +Compile all .cpp files and link them into your program. No special build tools +are required. OXT depends on a specially patched version of Boost. + diff --git a/ext/oxt/backtrace.cpp b/ext/oxt/backtrace.cpp new file mode 100644 index 00000000..ba984a64 --- /dev/null +++ b/ext/oxt/backtrace.cpp @@ -0,0 +1,172 @@ +/* + * OXT - OS eXtensions for boosT + * Provides important functionality necessary for writing robust server software. + * + * Copyright (c) 2008 Phusion + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#if !(defined(NDEBUG) || defined(OXT_DISABLE_BACKTRACES)) + +#include +#include +#include +#include "backtrace.hpp" + +namespace oxt { + +boost::mutex _thread_registration_mutex; +list _registered_threads; + +// Register main thread. +static initialize_backtrace_support_for_this_thread main_thread_initialization("Main thread"); + +/* + * boost::thread_specific_storage is pretty expensive. So we use the __thread + * keyword whenever possible - that's almost free. + * GCC supports the __thread keyword on x86 since version 3.3. Not sure + * about other architectures. + */ + +#if defined(__GNUC__) && ( \ + __GNUC__ > 3 || ( \ + __GNUC__ == 3 && __GNUC_MINOR__ >= 3 \ + ) \ +) + #define GCC_IS_3_3_OR_HIGHER +#endif + +/* + * FreeBSD 5 supports the __thread keyword, and everything works fine in + * micro-tests, but in mod_passenger the thread-local variables are initialized + * to unaligned addresses for some weird reason, thereby causing bus errors. + * + * GCC on OpenBSD supports __thread, but any access to such a variable + * results in a segfault. + * + * Solaris does support __thread, but often it's not compiled into default GCC + * packages (not to mention it's not available for Sparc). Playing it safe... + * + * MacOS X doesn't support __thread at all. + */ +#if defined(GCC_IS_3_3_OR_HIGHER) && !defined(__FreeBSD__) && \ + !defined(__SOLARIS__) && !defined(__OpenBSD__) && !defined(__APPLE__) + static __thread spin_lock *backtrace_lock = NULL; + static __thread vector *current_backtrace = NULL; + + void + _init_backtrace_tls() { + backtrace_lock = new spin_lock(); + current_backtrace = new vector(); + current_backtrace->reserve(50); + } + + void + _finalize_backtrace_tls() { + delete backtrace_lock; + delete current_backtrace; + } + + spin_lock * + _get_backtrace_lock() { + return backtrace_lock; + } + + vector * + _get_current_backtrace() { + return current_backtrace; + } +#else + static thread_specific_ptr backtrace_lock; + static thread_specific_ptr< vector > current_backtrace; + + void _init_backtrace_tls() { + // Not implemented. + } + + void _finalize_backtrace_tls() { + // Not implemented. + } + + spin_lock * + _get_backtrace_lock() { + spin_lock *result; + + result = backtrace_lock.get(); + if (OXT_UNLIKELY(result == NULL)) { + result = new spin_lock(); + backtrace_lock.reset(result); + } + return result; + } + + vector * + _get_current_backtrace() { + vector *result; + + result = current_backtrace.get(); + if (OXT_UNLIKELY(result == NULL)) { + result = new vector(); + current_backtrace.reset(result); + } + return result; + } +#endif + +template static string +format_backtrace(Iterable backtrace_list) { + if (backtrace_list->empty()) { + return " (empty)"; + } else { + stringstream result; + ReverseIterator it; + + for (it = backtrace_list->rbegin(); it != backtrace_list->rend(); it++) { + trace_point *p = *it; + + result << " in '" << p->function << "'"; + if (p->source != NULL) { + result << " (" << p->source << ":" << p->line << ")"; + } + result << endl; + } + return result.str(); + } +} + +string +_format_backtrace(const list *backtrace_list) { + return format_backtrace< + const list *, + list::const_reverse_iterator + >(backtrace_list); +} + +string +_format_backtrace(const vector *backtrace_list) { + return format_backtrace< + const vector *, + vector::const_reverse_iterator + >(backtrace_list); +} + +} // namespace oxt + +#endif + diff --git a/ext/oxt/backtrace.hpp b/ext/oxt/backtrace.hpp new file mode 100644 index 00000000..7d27cd95 --- /dev/null +++ b/ext/oxt/backtrace.hpp @@ -0,0 +1,135 @@ +/* + * OXT - OS eXtensions for boosT + * Provides important functionality necessary for writing robust server software. + * + * Copyright (c) 2008 Phusion + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#ifndef _OXT_BACKTRACE_HPP_ +#define _OXT_BACKTRACE_HPP_ + +/** + * Portable C++ backtrace support. + * + * C++ doesn't provide a builtin, automatic, portable way of obtaining + * backtraces. Obtaining a backtrace via a debugger (e.g. gdb) is very + * expensive. Furthermore, not all machines have a debugger installed. + * This makes it very hard to debug problems on production servers. + * + * This file provides a portable way of specifying and obtaining + * backtraces. Via oxt::thread::all_backtraces(), it is even possible + * to obtain the backtraces of all running threads. + * + *

Initialization

+ * Every thread that is to contain backtrace information must be + * initialized. Initialization is done by instantiating an + * initialize_backtrace_support_for_this_thread object. This includes the main + * thread as well. + * + * If you use oxt::thread, then initialization will be automatically done for + * you for that partciular thread. + * + *

Basic usage

+ * Backtrace points must be specified manually in the code using TRACE_POINT(). + * The TracableException class allows one to obtain the backtrace at the moment + * the exception object was created. + * + * For example: + * @code + * void foo() { + * TRACE_POINT(); + * do_something(); + * bar(); + * do_something_else(); + * } + * + * void bar() { + * TRACE_POINT(); + * throw TracableException(); + * } + * @encode + * + * One can obtain the backtrace string, as follows: + * @code + * try { + * foo(); + * } catch (const TracableException &e) { + * cout << "Something bad happened:\n" << e.backtrace(); + * } + * @endcode + * + * This will print something like: + * @code + * Something bad happened: + * in 'bar' (example.cpp:123) + * in 'foo' (example.cpp:117) + * in 'example_function' (example.cpp:456) + * @encode + * + *

Making sure the line number is correct

+ * A TRACE_POINT() call will add a backtrace point for the source line on + * which it is written. However, this causes an undesirable effect in long + * functions: + * @code + * 100 void some_long_function() { + * 101 TRACE_POINT(); + * 102 do_something(); + * 103 for (...) { + * 104 do_something(); + * 105 } + * 106 do_something_else(); + * 107 + * 108 if (!write_file()) { + * 109 throw TracableException(); + * 110 } + * 111 } + * @endcode + * You will want the thrown exception to show a backtrace line number that's + * near line 109. But as things are now, the backtrace will show line 101. + * + * This can be solved by calling UPDATE_TRACE_POINT() from time to time: + * @code + * 100 void some_long_function() { + * 101 TRACE_POINT(); + * 102 do_something(); + * 103 for (...) { + * 104 do_something(); + * 105 } + * 106 do_something_else(); + * 107 + * 108 if (!write_file()) { + * 109 UPDATE_TRACE_POINT(); // Added! + * 110 throw TracableException(); + * 111 } + * 112 } + * @endcode + * + *

Compilation options

+ * Define OXT_DISABLE_BACKTRACES to disable backtrace support. The backtrace + * functions as provided by this header will become empty stubs. + */ + +#if defined(NDEBUG) || defined(OXT_DISABLE_BACKTRACES) + #include "detail/backtrace_disabled.hpp" +#else + #include "detail/backtrace_enabled.hpp" +#endif + +#endif /* _OXT_BACKTRACE_HPP_ */ diff --git a/ext/oxt/detail/backtrace_disabled.hpp b/ext/oxt/detail/backtrace_disabled.hpp new file mode 100644 index 00000000..f98937f8 --- /dev/null +++ b/ext/oxt/detail/backtrace_disabled.hpp @@ -0,0 +1,39 @@ +/* + * OXT - OS eXtensions for boosT + * Provides important functionality necessary for writing robust server software. + * + * Copyright (c) 2008 Phusion + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include +#include + +// Dummy implementation for backtrace.hpp. +// See detail/backtrace_enabled.hpp for API documentation. + +namespace oxt { + +#define TRACE_POINT() do { /* nothing */ } while (false) +#define TRACE_POINT_WITH_NAME(name) do { /* nothing */ } while (false) +#define UPDATE_TRACE_POINT() do { /* nothing */ } while (false) + +} // namespace oxt + diff --git a/ext/oxt/detail/backtrace_enabled.hpp b/ext/oxt/detail/backtrace_enabled.hpp new file mode 100644 index 00000000..3fd50bff --- /dev/null +++ b/ext/oxt/detail/backtrace_enabled.hpp @@ -0,0 +1,155 @@ +/* + * OXT - OS eXtensions for boosT + * Provides important functionality necessary for writing robust server software. + * + * Copyright (c) 2008 Phusion + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +// Actual implementation for backtrace.hpp. + +#define OXT_BACKTRACE_IS_ENABLED + +#include +#include +#include +#include +#include +#include +#include "../spin_lock.hpp" +#include "../macros.hpp" + +namespace oxt { + +using namespace std; +using namespace boost; +class trace_point; +class tracable_exception; +class thread_registration; + +extern boost::mutex _thread_registration_mutex; +extern list _registered_threads; + +void _init_backtrace_tls(); +void _finalize_backtrace_tls(); +spin_lock *_get_backtrace_lock(); +vector *_get_current_backtrace(); +string _format_backtrace(const list *backtrace_list); +string _format_backtrace(const vector *backtrace_list); + +/** + * A single point in a backtrace. Creating this object will cause it + * to push itself to the thread's backtrace list. This backtrace list + * is stored in a thread local storage, and so is unique for each + * thread. Upon destruction, the object will pop itself from the thread's + * backtrace list. + * + * Except if you set the 'detached' argument to true. + * + * Implementation detail - do not use directly! + * @internal + */ +struct trace_point { + const char *function; + const char *source; + unsigned int line; + bool m_detached; + + trace_point(const char *function, const char *source, unsigned int line) { + this->function = function; + this->source = source; + this->line = line; + m_detached = false; + + spin_lock *lock = _get_backtrace_lock(); + if (OXT_LIKELY(lock != NULL)) { + spin_lock::scoped_lock l(*lock); + _get_current_backtrace()->push_back(this); + } + } + + trace_point(const char *function, const char *source, unsigned int line, bool detached) { + this->function = function; + this->source = source; + this->line = line; + m_detached = true; + } + + ~trace_point() { + if (OXT_LIKELY(!m_detached)) { + spin_lock *lock = _get_backtrace_lock(); + if (OXT_LIKELY(lock != NULL)) { + spin_lock::scoped_lock l(*lock); + _get_current_backtrace()->pop_back(); + } + } + } + + void update(const char *source, unsigned int line) { + this->source = source; + this->line = line; + } +}; + +#define TRACE_POINT() oxt::trace_point __p(BOOST_CURRENT_FUNCTION, __FILE__, __LINE__) +#define TRACE_POINT_WITH_NAME(name) oxt::trace_point __p(name, __FILE__, __LINE__) +#define UPDATE_TRACE_POINT() __p.update(__FILE__, __LINE__) + +/** + * @internal + */ +struct thread_registration { + string name; + spin_lock *backtrace_lock; + vector *backtrace; +}; + +/** + * @internal + */ +struct initialize_backtrace_support_for_this_thread { + thread_registration *registration; + list::iterator it; + + initialize_backtrace_support_for_this_thread(const string &name) { + _init_backtrace_tls(); + registration = new thread_registration(); + registration->name = name; + registration->backtrace_lock = _get_backtrace_lock(); + registration->backtrace = _get_current_backtrace(); + + boost::mutex::scoped_lock l(_thread_registration_mutex); + _registered_threads.push_back(registration); + it = _registered_threads.end(); + it--; + } + + ~initialize_backtrace_support_for_this_thread() { + { + boost::mutex::scoped_lock l(_thread_registration_mutex); + _registered_threads.erase(it); + delete registration; + } + _finalize_backtrace_tls(); + } +}; + +} // namespace oxt + diff --git a/ext/oxt/detail/spin_lock_gcc_x86.hpp b/ext/oxt/detail/spin_lock_gcc_x86.hpp new file mode 100644 index 00000000..5343941b --- /dev/null +++ b/ext/oxt/detail/spin_lock_gcc_x86.hpp @@ -0,0 +1,82 @@ +/* + * OXT - OS eXtensions for boosT + * Provides important functionality necessary for writing robust server software. + * + * Copyright (c) 2008 Phusion + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include + +namespace oxt { + +/** + * A spin lock. It's more efficient than a mutex for locking very small + * critical sections with few contentions, but less efficient otherwise. + * + * The interface is similar to that of boost::mutex. + */ +class spin_lock { +private: + int exclusion; + +public: + /** + * Instantiate this class to lock a spin lock within a scope. + */ + class scoped_lock: boost::noncopyable { + private: + spin_lock &l; + + public: + scoped_lock(spin_lock &lock): l(lock) { + l.lock(); + } + + ~scoped_lock() { + l.unlock(); + } + }; + + spin_lock(): exclusion(0) { } + + /** + * Lock this spin lock. + * @throws boost::thread_resource_error Something went wrong. + */ + void lock() { + while (__sync_lock_test_and_set(&exclusion, 1)) { + // Do nothing. This GCC builtin instruction + // ensures memory barrier. + } + } + + /** + * Unlock this spin lock. + * @throws boost::thread_resource_error Something went wrong. + */ + void unlock() { + __sync_synchronize(); // Memory barrier. + exclusion = 0; + } +}; + +} // namespace oxt + diff --git a/ext/oxt/detail/spin_lock_portable.hpp b/ext/oxt/detail/spin_lock_portable.hpp new file mode 100644 index 00000000..d2e3f332 --- /dev/null +++ b/ext/oxt/detail/spin_lock_portable.hpp @@ -0,0 +1,38 @@ +/* + * OXT - OS eXtensions for boosT + * Provides important functionality necessary for writing robust server software. + * + * Copyright (c) 2008 Phusion + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/* + * Portable implementation of a spin lock. + * This is actually just a mutex... + * + * See spin_lock_gcc_x86.hpp for API documentation. + */ + +#include + +namespace oxt { + typedef boost::mutex spin_lock; +} + diff --git a/ext/oxt/detail/spin_lock_pthreads.hpp b/ext/oxt/detail/spin_lock_pthreads.hpp new file mode 100644 index 00000000..f1f5d1da --- /dev/null +++ b/ext/oxt/detail/spin_lock_pthreads.hpp @@ -0,0 +1,97 @@ +/* + * OXT - OS eXtensions for boosT + * Provides important functionality necessary for writing robust server software. + * + * Copyright (c) 2008 Phusion + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include +#include +#include +#include "../macros.hpp" + +/* + * Implementation of a spin lock using POSIX pthread spin locks. + * + * See spin_lock_gcc_x86.hpp for API documentation. + */ + +namespace oxt { + +class spin_lock { +private: + pthread_spinlock_t spin; + +public: + class scoped_lock: boost::noncopyable { + private: + spin_lock &l; + + public: + scoped_lock(spin_lock &lock): l(lock) { + l.lock(); + } + + ~scoped_lock() { + l.unlock(); + } + }; + + spin_lock() { + int ret; + do { + ret = pthread_spin_init(&spin, PTHREAD_PROCESS_PRIVATE); + } while (ret == EINTR); + if (ret != 0) { + throw boost::thread_resource_error("Cannot initialize a spin lock", ret); + } + } + + ~spin_lock() { + int ret; + do { + ret = pthread_spin_destroy(&spin); + } while (ret == EINTR); + } + + void lock() { + int ret; + do { + ret = pthread_spin_lock(&spin); + } while (OXT_UNLIKELY(ret == EINTR)); + if (OXT_UNLIKELY(ret != 0)) { + throw boost::thread_resource_error("Cannot lock spin lock", ret); + } + } + + void unlock() { + int ret; + do { + ret = pthread_spin_unlock(&spin); + } while (OXT_UNLIKELY(ret == EINTR)); + if (OXT_UNLIKELY(ret != 0)) { + throw boost::thread_resource_error("Cannot unlock spin lock", ret); + } + } +}; + +} // namespace oxt + diff --git a/ext/oxt/detail/tracable_exception_disabled.hpp b/ext/oxt/detail/tracable_exception_disabled.hpp new file mode 100644 index 00000000..92090ab8 --- /dev/null +++ b/ext/oxt/detail/tracable_exception_disabled.hpp @@ -0,0 +1,46 @@ +/* + * OXT - OS eXtensions for boosT + * Provides important functionality necessary for writing robust server software. + * + * Copyright (c) 2008 Phusion + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include +#include + +// Dummy implementation for tracable_exception.hpp. +// See detail/tracable_exception_enabled.hpp for API documentation. + +namespace oxt { + +class tracable_exception: public std::exception { +public: + virtual std::string backtrace() const throw() { + return " (backtrace support disabled during compile time)\n"; + } + + virtual const char *what() const throw() { + return "oxt::tracable_exception"; + } +}; + +} // namespace oxt + diff --git a/ext/oxt/detail/tracable_exception_enabled.hpp b/ext/oxt/detail/tracable_exception_enabled.hpp new file mode 100644 index 00000000..e0bcc51d --- /dev/null +++ b/ext/oxt/detail/tracable_exception_enabled.hpp @@ -0,0 +1,48 @@ +/* + * OXT - OS eXtensions for boosT + * Provides important functionality necessary for writing robust server software. + * + * Copyright (c) 2008 Phusion + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#include +#include +#include + +namespace oxt { + +struct trace_point; + +/** + * Exception class with backtrace support. See backtrace.hpp for details. + */ +class tracable_exception: public std::exception { +private: + std::list backtrace_copy; +public: + tracable_exception(); + tracable_exception(const tracable_exception &other); + virtual ~tracable_exception() throw(); + virtual std::string backtrace() const throw(); + virtual const char *what() const throw(); +}; + +} // namespace oxt + diff --git a/ext/oxt/macros.hpp b/ext/oxt/macros.hpp new file mode 100644 index 00000000..6cfc7e65 --- /dev/null +++ b/ext/oxt/macros.hpp @@ -0,0 +1,58 @@ +/* + * OXT - OS eXtensions for boosT + * Provides important functionality necessary for writing robust server software. + * + * Copyright (c) 2008 Phusion + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#ifndef _OXT_MACROS_HPP_ +#define _OXT_MACROS_HPP_ + +/** + * Specialized macros. + * + * These macros provide more specialized features which are not needed + * so often by application programmers. + */ + +#if (defined(__GNUC__) && (__GNUC__ > 2) && !defined(OXT_DEBUG)) || defined(IN_DOXYGEN) + /** + * Indicate that the given expression is likely to be true. + * This allows the CPU to better perform branch prediction. + * + * Defining OXT_DEBUG will cause this macro to become an + * empty stub. + */ + #define OXT_LIKELY(expr) __builtin_expect((expr), 1) + + /** + * Indicate that the given expression is likely to be false. + * This allows the CPU to better perform branch prediction. + * + * Defining OXT_DEBUG will cause this macro to become an + * empty stub. + */ + #define OXT_UNLIKELY(expr) __builtin_expect((expr), 0) +#else + #define OXT_LIKELY(expr) expr + #define OXT_UNLIKELY(expr) expr +#endif + +#endif /* _OXT_MACROS_HPP_ */ diff --git a/ext/oxt/spin_lock.hpp b/ext/oxt/spin_lock.hpp new file mode 100644 index 00000000..66b9f90a --- /dev/null +++ b/ext/oxt/spin_lock.hpp @@ -0,0 +1,55 @@ +/* + * OXT - OS eXtensions for boosT + * Provides important functionality necessary for writing robust server software. + * + * Copyright (c) 2008 Phusion + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#ifndef _OXT_SPIN_LOCK_HPP_ +#define _OXT_SPIN_LOCK_HPP_ + +// At the time of writing (July 22, 2008), these operating systems don't +// support pthread spin locks: +// - OpenBSD 4.3 +// - Solaris 9 +// - MacOS X +#if defined(__OpenBSD__) || defined(__SOLARIS9__) || defined(__APPLE__) + #define OXT_NO_PTHREAD_SPINLOCKS +#endif + +#ifndef GCC_VERSION + #define GCC_VERSION (__GNUC__ * 10000 \ + + __GNUC_MINOR__ * 100 \ + + __GNUC_PATCH_LEVEL__) +#endif + +#if (GCC_VERSION > 40100 && defined(__i386__)) || defined(IN_DOXYGEN) + // GCC 4.0 doesn't support __sync instructions while GCC 4.2 + // does. I'm not sure whether support for it started in 4.1 or + // 4.2, so the above version check may have to be changed later. + #include "detail/spin_lock_gcc_x86.hpp" +#elif !defined(WIN32) && !defined(OXT_NO_PTHREAD_SPINLOCKS) + #include "detail/spin_lock_pthreads.hpp" +#else + #include "detail/spin_lock_portable.hpp" +#endif + +#endif /* _OXT_SPIN_LOCK_HPP_ */ + diff --git a/ext/apache2/System.cpp b/ext/oxt/system_calls.cpp similarity index 51% rename from ext/apache2/System.cpp rename to ext/oxt/system_calls.cpp index 44e1e170..96a7b6fd 100644 --- a/ext/apache2/System.cpp +++ b/ext/oxt/system_calls.cpp @@ -1,58 +1,50 @@ /* - * Phusion Passenger - http://www.modrails.com/ - * Copyright (C) 2008 Phusion + * OXT - OS eXtensions for boosT + * Provides important functionality necessary for writing robust server software. * - * Phusion Passenger is a trademark of Hongli Lai & Ninh Bui. + * 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. + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: * - * 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. + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. * - * 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. + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. */ -#include "System.h" - -/************************************* - * boost::this_thread - *************************************/ +#include "system_calls.hpp" +#include +#include using namespace boost; - -thread_specific_ptr this_thread::_syscalls_interruptable; - - -bool -this_thread::syscalls_interruptable() { - return _syscalls_interruptable.get() == NULL || *_syscalls_interruptable; -} +using namespace oxt; /************************************* - * Passenger + * oxt *************************************/ -using namespace Passenger; - -static bool interrupted = false; - static void -interruptionSignalHandler(int sig) { - interrupted = true; +interruption_signal_handler(int sig) { + // Do nothing. } void -Passenger::setupSyscallInterruptionSupport() { +oxt::setup_syscall_interruption_support() { struct sigaction action; int ret; - action.sa_handler = interruptionSignalHandler; + action.sa_handler = interruption_signal_handler; action.sa_flags = 0; sigemptyset(&action.sa_mask); do { @@ -65,7 +57,7 @@ Passenger::setupSyscallInterruptionSupport() { /************************************* - * Passenger::InterruptableCalls + * Passenger::syscalls *************************************/ #define CHECK_INTERRUPTION(error_expression, code) \ @@ -83,7 +75,7 @@ Passenger::setupSyscallInterruptionSupport() { } while (false) ssize_t -InterruptableCalls::read(int fd, void *buf, size_t count) { +syscalls::read(int fd, void *buf, size_t count) { ssize_t ret; CHECK_INTERRUPTION( ret == -1, @@ -93,7 +85,7 @@ InterruptableCalls::read(int fd, void *buf, size_t count) { } ssize_t -InterruptableCalls::write(int fd, const void *buf, size_t count) { +syscalls::write(int fd, const void *buf, size_t count) { ssize_t ret; CHECK_INTERRUPTION( ret == -1, @@ -103,7 +95,7 @@ InterruptableCalls::write(int fd, const void *buf, size_t count) { } int -InterruptableCalls::close(int fd) { +syscalls::close(int fd) { int ret; CHECK_INTERRUPTION( ret == -1, @@ -113,7 +105,27 @@ InterruptableCalls::close(int fd) { } int -InterruptableCalls::socketpair(int d, int type, int protocol, int sv[2]) { +syscalls::connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen) { + int ret; + CHECK_INTERRUPTION( + ret == -1, + ret = ::connect(sockfd, serv_addr, addrlen); + ); + return ret; +} + +int +syscalls::socket(int domain, int type, int protocol) { + int ret; + CHECK_INTERRUPTION( + ret == -1, + ret = ::socket(domain, type, protocol) + ); + return ret; +} + +int +syscalls::socketpair(int d, int type, int protocol, int sv[2]) { int ret; CHECK_INTERRUPTION( ret == -1, @@ -123,7 +135,7 @@ InterruptableCalls::socketpair(int d, int type, int protocol, int sv[2]) { } ssize_t -InterruptableCalls::recvmsg(int s, struct msghdr *msg, int flags) { +syscalls::recvmsg(int s, struct msghdr *msg, int flags) { ssize_t ret; CHECK_INTERRUPTION( ret == -1, @@ -133,7 +145,7 @@ InterruptableCalls::recvmsg(int s, struct msghdr *msg, int flags) { } ssize_t -InterruptableCalls::sendmsg(int s, const struct msghdr *msg, int flags) { +syscalls::sendmsg(int s, const struct msghdr *msg, int flags) { ssize_t ret; CHECK_INTERRUPTION( ret == -1, @@ -143,7 +155,17 @@ InterruptableCalls::sendmsg(int s, const struct msghdr *msg, int flags) { } int -InterruptableCalls::shutdown(int s, int how) { +syscalls::setsockopt(int s, int level, int optname, const void *optval, socklen_t optlen) { + int ret; + CHECK_INTERRUPTION( + ret == -1, + ret = ::setsockopt(s, level, optname, optval, optlen) + ); + return ret; +} + +int +syscalls::shutdown(int s, int how) { int ret; CHECK_INTERRUPTION( ret == -1, @@ -153,7 +175,7 @@ InterruptableCalls::shutdown(int s, int how) { } FILE * -InterruptableCalls::fopen(const char *path, const char *mode) { +syscalls::fopen(const char *path, const char *mode) { FILE *ret; CHECK_INTERRUPTION( ret == NULL, @@ -163,7 +185,7 @@ InterruptableCalls::fopen(const char *path, const char *mode) { } int -InterruptableCalls::fclose(FILE *fp) { +syscalls::fclose(FILE *fp) { int ret; CHECK_INTERRUPTION( ret == EOF, @@ -173,7 +195,7 @@ InterruptableCalls::fclose(FILE *fp) { } time_t -InterruptableCalls::time(time_t *t) { +syscalls::time(time_t *t) { time_t ret; CHECK_INTERRUPTION( ret == (time_t) -1, @@ -183,15 +205,15 @@ InterruptableCalls::time(time_t *t) { } int -InterruptableCalls::usleep(useconds_t usec) { +syscalls::usleep(useconds_t usec) { struct timespec spec; spec.tv_sec = usec / 1000000; spec.tv_nsec = usec % 1000000; - return InterruptableCalls::nanosleep(&spec, NULL); + return syscalls::nanosleep(&spec, NULL); } int -InterruptableCalls::nanosleep(const struct timespec *req, struct timespec *rem) { +syscalls::nanosleep(const struct timespec *req, struct timespec *rem) { struct timespec req2 = *req; struct timespec rem2; int ret, e; @@ -211,7 +233,7 @@ InterruptableCalls::nanosleep(const struct timespec *req, struct timespec *rem) } pid_t -InterruptableCalls::fork() { +syscalls::fork() { int ret; CHECK_INTERRUPTION( ret == -1, @@ -221,7 +243,7 @@ InterruptableCalls::fork() { } int -InterruptableCalls::kill(pid_t pid, int sig) { +syscalls::kill(pid_t pid, int sig) { int ret; CHECK_INTERRUPTION( ret == -1, @@ -231,7 +253,7 @@ InterruptableCalls::kill(pid_t pid, int sig) { } pid_t -InterruptableCalls::waitpid(pid_t pid, int *status, int options) { +syscalls::waitpid(pid_t pid, int *status, int options) { pid_t ret; CHECK_INTERRUPTION( ret == -1, @@ -240,3 +262,16 @@ InterruptableCalls::waitpid(pid_t pid, int *status, int options) { return ret; } + +/************************************* + * boost::this_thread + *************************************/ + +thread_specific_ptr this_thread::_syscalls_interruptable; + + +bool +this_thread::syscalls_interruptable() { + return _syscalls_interruptable.get() == NULL || *_syscalls_interruptable; +} + diff --git a/ext/oxt/system_calls.hpp b/ext/oxt/system_calls.hpp new file mode 100644 index 00000000..d57f9125 --- /dev/null +++ b/ext/oxt/system_calls.hpp @@ -0,0 +1,232 @@ +/* + * OXT - OS eXtensions for boosT + * Provides important functionality necessary for writing robust server software. + * + * Copyright (c) 2008 Phusion + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#ifndef _OXT_SYSTEM_CALLS_HPP_ +#define _OXT_SYSTEM_CALLS_HPP_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** + * Support for interruption of blocking system calls and C library calls + * + * This file provides a framework for writing multithreading code that can + * be interrupted, even when blocked on system calls or C library calls. + * + * One must first call oxt::setup_syscall_interruption_support(). + * Then one may use the functions in oxt::syscalls as drop-in replacements + * for system calls or C library functions. These functions throw + * boost::thread_interrupted upon interruption. + * + * System call interruption is disabled by default. In other words: the + * replacement functions in this file don't throw boost::thread_interrupted. + * You can enable or disable system call interruption in the current scope + * by creating instances of boost::this_thread::enable_syscall_interruption + * and similar objects. This is similar to Boost thread interruption. + * + *

How to interrupt

+ * Generally, oxt::thread::interrupt() and oxt::thread::interrupt_and_join() + * should be used for interrupting threads. These methods will interrupt + * the thread at all Boost interruption points, as well as system calls that + * are caled through the oxt::syscalls namespace. Do *not* use + * boost::thread::interrupt, because that will not honor system calls as + * interruption points. + * + * Under the hood, system calls are interrupted by sending a signal to the + * to a specific thread (note: sending a signal to a process will deliver the + * signal to the main thread). + * + * Any signal will do, but of course, one should only send a signal whose + * signal handler doesn't do undesirable things (such as aborting the entire + * program). That's why it's generally recommended that you only use + * oxt::INTERRUPTION_SIGNAL to interrupt system calls, because + * oxt::setup_syscall_interruption_support() installs an "nice" signal + * handler for that signal (though you should of course use + * oxt::thread::interrupt() instead of sending signals whenever + * possible). + * + * Note that sending a signal once may not interrupt the thread, because + * the thread may not be calling a system call at the time the signal was + * received. So one must keep sending signals periodically until the + * thread has quit. + * + * @warning + * After oxt::setup_syscall_interruption_support() is called, sending a signal + * will cause system calls to return with an EINTR error. The oxt::syscall + * functions will automatically take care of this, but if you're calling any + * system calls without using that namespace, then you should check for and + * take care of EINTR errors. + */ + +// This is one of the things that Java is good at and C++ sucks at. Sigh... + +namespace oxt { + static const int INTERRUPTION_SIGNAL = SIGINT; + + /** + * Setup system call interruption support. + * This function may only be called once. It installs a signal handler + * for INTERRUPTION_SIGNAL, so one should not install a different signal + * handler for that signal after calling this function. + * + * @warning + * After oxt::setup_syscall_interruption_support() is called, sending a signal + * will cause system calls to return with an EINTR error. The oxt::syscall + * functions will automatically take care of this, but if you're calling any + * system calls without using that namespace, then you should check for and + * take care of EINTR errors. + */ + void setup_syscall_interruption_support(); + + /** + * System call and C library call wrappers with interruption support. + * These functions are interruption points, i.e. they throw + * boost::thread_interrupted whenever the calling thread is interrupted + * by oxt::thread::interrupt() or oxt::thread::interrupt_and_join(). + */ + namespace syscalls { + ssize_t read(int fd, void *buf, size_t count); + ssize_t write(int fd, const void *buf, size_t count); + int close(int fd); + + int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen); + int socket(int domain, int type, int protocol); + int socketpair(int d, int type, int protocol, int sv[2]); + ssize_t recvmsg(int s, struct msghdr *msg, int flags); + ssize_t sendmsg(int s, const struct msghdr *msg, int flags); + int setsockopt(int s, int level, int optname, const void *optval, + socklen_t optlen); + int shutdown(int s, int how); + + FILE *fopen(const char *path, const char *mode); + int fclose(FILE *fp); + + time_t time(time_t *t); + int usleep(useconds_t usec); + int nanosleep(const struct timespec *req, struct timespec *rem); + + pid_t fork(); + int kill(pid_t pid, int sig); + pid_t waitpid(pid_t pid, int *status, int options); + } + +} // namespace oxt + +namespace boost { +namespace this_thread { + + /** + * @intern + */ + extern thread_specific_ptr _syscalls_interruptable; + + /** + * Check whether system calls should be interruptable in + * the calling thread. + */ + bool syscalls_interruptable(); + + class restore_syscall_interruption; + + /** + * Create this struct on the stack to temporarily enable system + * call interruption, until the object goes out of scope. + */ + class enable_syscall_interruption { + private: + bool last_value; + public: + enable_syscall_interruption() { + if (_syscalls_interruptable.get() == NULL) { + last_value = true; + _syscalls_interruptable.reset(new bool(true)); + } else { + last_value = *_syscalls_interruptable; + *_syscalls_interruptable = true; + } + } + + ~enable_syscall_interruption() { + *_syscalls_interruptable = last_value; + } + }; + + /** + * Create this struct on the stack to temporarily disable system + * call interruption, until the object goes out of scope. + * While system call interruption is disabled, the functions in + * InterruptableCalls will try until the return code is not EINTR. + */ + class disable_syscall_interruption { + private: + friend class restore_syscall_interruption; + bool last_value; + public: + disable_syscall_interruption() { + if (_syscalls_interruptable.get() == NULL) { + last_value = true; + _syscalls_interruptable.reset(new bool(false)); + } else { + last_value = *_syscalls_interruptable; + *_syscalls_interruptable = false; + } + } + + ~disable_syscall_interruption() { + *_syscalls_interruptable = last_value; + } + }; + + /** + * Creating an object of this class on the stack will restore the + * system call interruption state to what it was before. + */ + class restore_syscall_interruption { + private: + int last_value; + public: + restore_syscall_interruption(const disable_syscall_interruption &intr) { + assert(_syscalls_interruptable.get() != NULL); + last_value = *_syscalls_interruptable; + *_syscalls_interruptable = intr.last_value; + } + + ~restore_syscall_interruption() { + *_syscalls_interruptable = last_value; + } + }; + +} // namespace this_thread +} // namespace boost + +#endif /* _OXT_SYSTEM_CALLS_HPP_ */ + diff --git a/ext/oxt/thread.cpp b/ext/oxt/thread.cpp new file mode 100644 index 00000000..92a1518d --- /dev/null +++ b/ext/oxt/thread.cpp @@ -0,0 +1,32 @@ +/* + * OXT - OS eXtensions for boosT + * Provides important functionality necessary for writing robust server software. + * + * Copyright (c) 2008 Phusion + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "thread.hpp" + +namespace oxt { + boost::mutex _next_thread_number_mutex; + unsigned int _next_thread_number = 0; +} + diff --git a/ext/oxt/thread.hpp b/ext/oxt/thread.hpp new file mode 100644 index 00000000..439b3e25 --- /dev/null +++ b/ext/oxt/thread.hpp @@ -0,0 +1,223 @@ +/* + * OXT - OS eXtensions for boosT + * Provides important functionality necessary for writing robust server software. + * + * Copyright (c) 2008 Phusion + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#ifndef _OXT_THREAD_HPP_ +#define _OXT_THREAD_HPP_ + +#include +#include +#include +#include +#include "system_calls.hpp" +#include "backtrace.hpp" +#ifdef OXT_BACKTRACE_IS_ENABLED + #include +#endif + +namespace oxt { + +extern boost::mutex _next_thread_number_mutex; +extern unsigned int _next_thread_number; + +/** + * Enhanced thread class with support for: + * - user-defined stack size. + * - system call interruption. + * - backtraces. + */ +class thread: public boost::thread { +private: + struct thread_data { + std::string name; + #ifdef OXT_BACKTRACE_IS_ENABLED + thread_registration *registration; + boost::mutex registration_lock; + bool done; + #endif + }; + + typedef boost::shared_ptr thread_data_ptr; + + thread_data_ptr data; + + void initialize_data(const std::string &thread_name) { + data = thread_data_ptr(new thread_data()); + if (thread_name.empty()) { + boost::mutex::scoped_lock l(_next_thread_number_mutex); + std::stringstream str; + + str << "Thread #" << _next_thread_number; + _next_thread_number++; + data->name = str.str(); + } else { + data->name = thread_name; + } + #ifdef OXT_BACKTRACE_IS_ENABLED + data->registration = NULL; + data->done = false; + #endif + } + + static void thread_main(boost::function func, thread_data_ptr data) { + #ifdef OXT_BACKTRACE_IS_ENABLED + initialize_backtrace_support_for_this_thread i(data->name); + data->registration = i.registration; + #endif + + #ifdef OXT_BACKTRACE_IS_ENABLED + // Put finalization code in a struct destructor, + // for exception safety. + struct finalization_routines { + thread_data_ptr &data; + + finalization_routines(thread_data_ptr &data_) + : data(data_) {} + + ~finalization_routines() { + boost::mutex::scoped_lock l(data->registration_lock); + data->registration = NULL; + data->done = true; + } + }; + finalization_routines f(data); + #endif + + func(); + } + +public: + /** + * Create a new thread. + * + * @param func A function object which will be called as the thread's + * main function. This object must be copyable. func is + * copied into storage managed internally by the thread library, + * and that copy is invoked on a newly-created thread of execution. + * @param name A name for this thread. If an empty string is given, then + * a name will be automatically chosen. + * @param stack_size The stack size, in bytes, that the thread should + * have. If 0 is specified, the operating system's default stack + * size is used. + * @pre func must be copyable. + * @throws boost::thread_resource_error Something went wrong during + * creation of the thread. + */ + explicit thread(boost::function func, const std::string &name = "", unsigned int stack_size = 0) { + initialize_data(name); + + set_thread_main_function(boost::bind(thread_main, func, data)); + start_thread(stack_size); + } + + /** + * Return this thread's name. The name was set during construction. + */ + std::string name() const throw() { + return data->name; + } + + /** + * Return the current backtrace of the thread of execution, as a string. + */ + std::string backtrace() const throw() { + #ifdef OXT_BACKTRACE_IS_ENABLED + boost::mutex::scoped_lock l(data->registration_lock); + if (data->registration == NULL) { + if (data->done) { + return " (no backtrace: thread has quit)"; + } else { + return " (no backtrace: thread hasn't been started yet)"; + } + } else { + spin_lock::scoped_lock l2(*data->registration->backtrace_lock); + return _format_backtrace(data->registration->backtrace); + } + #else + return " (backtrace support disabled during compile time)"; + #endif + } + + /** + * Return the backtraces of all oxt::thread threads, as well as that of the + * main thread, in a nicely formatted string. + */ + static std::string all_backtraces() throw() { + #ifdef OXT_BACKTRACE_IS_ENABLED + boost::mutex::scoped_lock l(_thread_registration_mutex); + list::const_iterator it; + std::stringstream result; + + for (it = _registered_threads.begin(); it != _registered_threads.end(); it++) { + thread_registration *r = *it; + result << "Thread '" << r->name << "':" << endl; + + spin_lock::scoped_lock l(*r->backtrace_lock); + result << _format_backtrace(r->backtrace) << endl; + } + return result.str(); + #else + return "(backtrace support disabled during compile time)"; + #endif + } + + /** + * Interrupt the thread. This method behaves just like + * boost::thread::interrupt(), but will also respect the interruption + * points defined in oxt::syscalls. + * + * Note that an interruption request may get lost, depending on the + * current execution point of the thread. Thus, one should call this + * method in a loop, until a certain goal condition has been fulfilled. + * interrupt_and_join() is a convenience method that implements this + * pattern. + */ + void interrupt() { + int ret; + + boost::thread::interrupt(); + do { + ret = pthread_kill(native_handle(), + INTERRUPTION_SIGNAL); + } while (ret == EINTR); + } + + /** + * Keep interrupting the thread until it's done, then join it. + * + * @throws boost::thread_interrupted The calling thread has been + * interrupted before we could join this thread. + */ + void interrupt_and_join() { + bool done = false; + while (!done) { + interrupt(); + done = timed_join(boost::posix_time::millisec(10)); + } + } +}; + +} // namespace oxt + +#endif /* _OXT_THREAD_HPP_ */ + diff --git a/ext/oxt/tracable_exception.cpp b/ext/oxt/tracable_exception.cpp new file mode 100644 index 00000000..f3122b30 --- /dev/null +++ b/ext/oxt/tracable_exception.cpp @@ -0,0 +1,87 @@ +/* + * OXT - OS eXtensions for boosT + * Provides important functionality necessary for writing robust server software. + * + * Copyright (c) 2008 Phusion + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#include "tracable_exception.hpp" +#include "backtrace.hpp" + +#ifdef OXT_BACKTRACE_IS_ENABLED + +#include +#include "macros.hpp" + +namespace oxt { + +using namespace std; + +tracable_exception::tracable_exception() { + spin_lock *lock = _get_backtrace_lock(); + if (OXT_LIKELY(lock != NULL)) { + spin_lock::scoped_lock l(*lock); + vector *bt = _get_current_backtrace(); + vector::const_iterator it; + + for (it = bt->begin(); it != bt->end(); it++) { + trace_point *p = new trace_point( + (*it)->function, + (*it)->source, + (*it)->line, + true); + backtrace_copy.push_back(p); + } + } +} + +tracable_exception::tracable_exception(const tracable_exception &other) { + list::const_iterator it; + for (it = other.backtrace_copy.begin(); it != other.backtrace_copy.end(); it++) { + trace_point *p = new trace_point( + (*it)->function, + (*it)->source, + (*it)->line, + true); + backtrace_copy.push_back(p); + } +} + +tracable_exception::~tracable_exception() throw() { + list::iterator it; + for (it = backtrace_copy.begin(); it != backtrace_copy.end(); it++) { + delete *it; + } +} + +string +tracable_exception::backtrace() const throw() { + return _format_backtrace(&backtrace_copy); +} + +const char * +tracable_exception::what() const throw() { + return "oxt::tracable_exception"; +} + +} // namespace oxt + +#endif + diff --git a/ext/oxt/tracable_exception.hpp b/ext/oxt/tracable_exception.hpp new file mode 100644 index 00000000..95004daf --- /dev/null +++ b/ext/oxt/tracable_exception.hpp @@ -0,0 +1,35 @@ +/* + * OXT - OS eXtensions for boosT + * Provides important functionality necessary for writing robust server software. + * + * Copyright (c) 2008 Phusion + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#ifndef _OXT_TRACABLE_EXCEPTION_HPP_ +#define _OXT_TRACABLE_EXCEPTION_HPP_ + +#if defined(NDEBUG) || defined(OXT_DISABLE_BACKTRACES) + #include "detail/tracable_exception_disabled.hpp" +#else + #include "detail/tracable_exception_enabled.hpp" +#endif + +#endif /* _OXT_TRACABLE_EXCEPTION_HPP_ */ + diff --git a/ext/passenger/extconf.rb b/ext/phusion_passenger/extconf.rb similarity index 79% rename from ext/passenger/extconf.rb rename to ext/phusion_passenger/extconf.rb index c7ac3e77..1d41cb0f 100644 --- a/ext/passenger/extconf.rb +++ b/ext/phusion_passenger/extconf.rb @@ -17,4 +17,15 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. require 'mkmf' $LIBS="" -create_makefile('native_support') + +if RUBY_PLATFORM =~ /solaris/ + have_library('xnet') + $CFLAGS << " -D_XPG4_2" + if RUBY_PLATFORM =~ /solaris2.9/ + $CFLAGS << " -D__SOLARIS9__" + end +end + +with_cflags($CFLAGS) do + create_makefile('native_support') +end diff --git a/ext/passenger/native_support.c b/ext/phusion_passenger/native_support.c similarity index 91% rename from ext/passenger/native_support.c rename to ext/phusion_passenger/native_support.c index fba78118..dcdd83d7 100644 --- a/ext/passenger/native_support.c +++ b/ext/phusion_passenger/native_support.c @@ -24,11 +24,22 @@ #include #include #include +#ifdef __OpenBSD__ + // OpenBSD needs this for 'struct iovec'. Apparently it isn't + // always included by unistd.h and sys/types.h. + #include +#endif #define MIN(a, b) (((a) < (b)) ? (a) : (b)) #ifndef RARRAY_LEN #define RARRAY_LEN(ary) RARRAY(ary)->len #endif +#ifndef RSTRING_PTR + #define RSTRING_PTR(str) RSTRING(str)->ptr +#endif +#ifndef RSTRING_LEN + #define RSTRING_LEN(str) RSTRING(str)->len +#endif static VALUE mPassenger; static VALUE mNativeSupport; @@ -48,7 +59,7 @@ send_fd(VALUE self, VALUE socket_fd, VALUE fd_to_send) { struct msghdr msg; struct iovec vec; char dummy[1]; - #ifdef __APPLE__ + #if defined(__APPLE__) || defined(__SOLARIS__) struct { struct cmsghdr header; int fd; @@ -77,7 +88,7 @@ send_fd(VALUE self, VALUE socket_fd, VALUE fd_to_send) { control_header->cmsg_level = SOL_SOCKET; control_header->cmsg_type = SCM_RIGHTS; control_payload = NUM2INT(fd_to_send); - #ifdef __APPLE__ + #if defined(__APPLE__) || defined(__SOLARIS__) control_header->cmsg_len = sizeof(control_data); control_data.fd = control_payload; #else @@ -108,7 +119,7 @@ recv_fd(VALUE self, VALUE socket_fd) { struct msghdr msg; struct iovec vec; char dummy[1]; - #ifdef __APPLE__ + #if defined(__APPLE__) || defined(__SOLARIS__) // File descriptor passing macros (CMSG_*) seem to be broken // on 64-bit MacOS X. This structure works around the problem. struct { @@ -147,7 +158,7 @@ recv_fd(VALUE self, VALUE socket_fd) { rb_sys_fail("No valid file descriptor received."); return Qnil; } - #ifdef __APPLE__ + #if defined(__APPLE__) || defined(__SOLARIS__) return INT2NUM(control_data.fd); #else return INT2NUM(*((int *) CMSG_DATA(control_header))); @@ -173,7 +184,8 @@ create_unix_socket(VALUE self, VALUE filename, VALUE backlog) { char *filename_str; long filename_length; - filename_str = rb_str2cstr(filename, &filename_length); + filename_str = RSTRING_PTR(filename); + filename_length = RSTRING_LEN(filename); fd = socket(PF_UNIX, SOCK_STREAM, 0); if (fd == -1) { @@ -256,7 +268,7 @@ Init_native_support() { struct sockaddr_un addr; /* */ - mPassenger = rb_define_module("Passenger"); // Do not remove the above comment. We want the Passenger module's rdoc to be empty. + mPassenger = rb_define_module("PhusionPassenger"); // Do not remove the above comment. We want the Passenger module's rdoc to be empty. /* * Utility functions for accessing system functionality. diff --git a/lib/passenger/platform_info.rb b/lib/passenger/platform_info.rb deleted file mode 100644 index e8fc17ed..00000000 --- a/lib/passenger/platform_info.rb +++ /dev/null @@ -1,302 +0,0 @@ -# Phusion Passenger - http://www.modrails.com/ -# Copyright (C) 2008 Phusion -# -# Phusion Passenger is a trademark of Hongli Lai & Ninh Bui. -# -# 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 'rbconfig' - -# Wow, I can't believe in how many ways one can build Apache in OS -# X! We have to resort to all sorts of tricks to make Passenger build -# out of the box on OS X. :-( -# -# In the name of usability and the "end user is the king" line of thought, -# I shall suffer the horrible faith of writing tons of autodetection code! - -# This module autodetects various platform-specific information, and -# provides that information through constants. -# -# Users can change the detection behavior by setting the environment variable -# APXS2 to the correct 'apxs' (or 'apxs2') binary, as provided by -# Apache. -module PlatformInfo -private - def self.env_defined?(name) - return !ENV[name].nil? && !ENV[name].empty? - end - - def self.determine_gem_command - gem_exe_in_path = find_command("gem") - correct_gem_exe = File.dirname(RUBY) + "/gem" - if gem_exe_in_path.nil? || gem_exe_in_path == correct_gem_exe - return "gem" - else - return correct_gem_exe - end - end - - def self.find_apxs2 - if env_defined?("APXS2") - return ENV["APXS2"] - end - ['apxs2', 'apxs'].each do |name| - command = find_command(name) - if !command.nil? - return command - end - end - return nil - end - - def self.determine_apache2_bindir - if APXS2.nil? - return nil - else - return `#{APXS2} -q BINDIR 2>/dev/null`.strip - end - end - - def self.determine_apache2_sbindir - if APXS2.nil? - return nil - else - return `#{APXS2} -q SBINDIR`.strip - end - end - - def self.find_apache2_executable(*possible_names) - [APACHE2_BINDIR, APACHE2_SBINDIR].each do |bindir| - if bindir.nil? - next - end - possible_names.each do |name| - filename = "#{bindir}/#{name}" - if File.file?(filename) && File.executable?(filename) - return filename - end - end - end - return nil - end - - def self.find_apache2ctl - return find_apache2_executable('apache2ctl', 'apachectl') - end - - def self.find_httpd - if env_defined?('HTTPD') - return ENV['HTTPD'] - elsif APXS2.nil? - ["apache2", "httpd2", "apache", "httpd"].each do |name| - command = find_command(name) - if !command.nil? - return command - end - end - return nil - else - return find_apache2_executable(`#{APXS2} -q TARGET`.strip) - end - end - - def self.determine_apxs2_flags - if APXS2.nil? - return nil - else - flags = `#{APXS2} -q CFLAGS`.strip << " -I" << `#{APXS2} -q INCLUDEDIR` - flags.strip! - flags.gsub!(/-O\d? /, '') - return flags - end - end - - def self.find_apr_config - if env_defined?('APR_CONFIG') - apr_config = ENV['APR_CONFIG'] - elsif RUBY_PLATFORM =~ /darwin/ && HTTPD == "/usr/sbin/httpd" - # If we're on MacOS X, and we're compiling against the - # default provided Apache, then we'll want to query the - # correct 'apr-1-config' command. However, that command - # is not in $PATH by default. Instead, it lives in - # /Developer/SDKs/MacOSX*sdk/usr/bin. - sdk_dir = Dir["/Developer/SDKs/MacOSX*sdk"].sort.last - if sdk_dir - apr_config = "#{sdk_dir}/usr/bin/apr-1-config" - if !File.executable?(apr_config) - apr_config = nil - end - end - else - apr_config = find_command('apr-1-config') - if apr_config.nil? - apr_config = find_command('apr-config') - end - end - return apr_config - end - - def self.determine_apr_info - if APR_CONFIG.nil? - return nil - else - flags = `#{APR_CONFIG} --cppflags --includes`.strip - libs = `#{APR_CONFIG} --link-ld`.strip - flags.gsub!(/-O\d? /, '') - return [flags, libs] - end - end - - def self.find_apu_config - if env_defined?('APU_CONFIG') - apu_config = ENV['APU_CONFIG'] - elsif RUBY_PLATFORM =~ /darwin/ && HTTPD == "/usr/sbin/httpd" - # If we're on MacOS X, and we're compiling against the - # default provided Apache, then we'll want to query the - # correct 'apu-1-config' command. However, that command - # is not in $PATH by default. Instead, it lives in - # /Developer/SDKs/MacOSX*sdk/usr/bin. - sdk_dir = Dir["/Developer/SDKs/MacOSX*sdk"].sort.last - if sdk_dir - apu_config = "#{sdk_dir}/usr/bin/apu-1-config" - if !File.executable?(apu_config) - apu_config = nil - end - end - else - apu_config = find_command('apu-1-config') - if apu_config.nil? - apu_config = find_command('apu-config') - end - end - return apu_config - end - - def self.determine_apu_info - if APU_CONFIG.nil? - return nil - else - flags = `#{APU_CONFIG} --includes`.strip - libs = `#{APU_CONFIG} --link-ld`.strip - flags.gsub!(/-O\d? /, '') - return [flags, libs] - end - end - - def self.determine_multi_arch_flags - if RUBY_PLATFORM =~ /darwin/ && !HTTPD.nil? - architectures = [] - `file "#{HTTPD}"`.split("\n").grep(/for architecture/).each do |line| - line =~ /for architecture (.*?)\)/ - architectures << "-arch #{$1}" - end - return architectures.join(' ') - else - return "" - end - end - - def self.determine_library_extension - if RUBY_PLATFORM =~ /darwin/ - return "bundle" - else - return "so" - end - end - - def self.read_file(filename) - return File.read(filename) - rescue - return "" - end - - def self.determine_linux_distro - if RUBY_PLATFORM !~ /linux/ - return nil - end - lsb_release = read_file("/etc/lsb-release") - if lsb_release =~ /Ubuntu/ - return :ubuntu - elsif File.exist?("/etc/debian_version") - return :debian - elsif File.exist?("/etc/redhat-release") - redhat_release = read_file("/etc/redhat-release") - if redhat_release =~ /CentOS/ - return :centos - elsif redhat_release =~ /Fedora/ # is this correct? - return :fedora - else - # On official RHEL distros, the content is in the form of - # "Red Hat Enterprise Linux Server release 5.1 (Tikanga)" - return :rhel - end - elsif File.exist?("/etc/suse-release") - return :suse - elsif File.exist?("/etc/gentoo-release") - return :gentoo - else - return :unknown - end - # TODO: Slackware, Mandrake/Mandriva - end - -public - # Check whether the specified command is in $PATH, and return its - # absolute filename. Returns nil if the command is not found. - # - # This function exists because system('which') doesn't always behave - # correctly, for some weird reason. - def self.find_command(name) - ENV['PATH'].split(File::PATH_SEPARATOR).detect do |directory| - path = File.join(directory, name.to_s) - if File.executable?(path) - return path - end - end - return nil - end - - # The absolute path to the current Ruby interpreter. - RUBY = Config::CONFIG['bindir'] + '/' + Config::CONFIG['RUBY_INSTALL_NAME'] + Config::CONFIG['EXEEXT'] - # The correct 'gem' command for this Ruby interpreter. - GEM = determine_gem_command - - # The absolute path to the 'apxs' or 'apxs2' executable. - APXS2 = find_apxs2 - # The absolute path to the Apache 2 'bin' directory. - APACHE2_BINDIR = determine_apache2_bindir - # The absolute path to the Apache 2 'sbin' directory. - APACHE2_SBINDIR = determine_apache2_sbindir - # The absolute path to the 'apachectl' or 'apache2ctl' binary. - APACHE2CTL = find_apache2ctl - # The absolute path to the Apache binary (that is, 'httpd', 'httpd2', 'apache' or 'apache2'). - HTTPD = find_httpd - # The absolute path to the 'apr-config' or 'apr-1-config' executable. - APR_CONFIG = find_apr_config - APU_CONFIG = find_apu_config - - # The C compiler flags that are necessary to compile an Apache module. - APXS2_FLAGS = determine_apxs2_flags - # The C compiler flags that are necessary for programs that use APR. - APR_FLAGS, APR_LIBS = determine_apr_info - # The C compiler flags that are necessary for programs that use APR-Util. - APU_FLAGS, APU_LIBS = determine_apu_info - - # The C compiler flags that are necessary for building binaries in the same architecture(s) as Apache. - MULTI_ARCH_FLAGS = determine_multi_arch_flags - # The current platform's shared library extension ('so' on most Unices). - LIBEXT = determine_library_extension - # An identifier for the current Linux distribution. nil if the operating system is not Linux. - LINUX_DISTRO = determine_linux_distro -end diff --git a/lib/passenger/templates/app_exited_during_initialization.html.erb b/lib/passenger/templates/app_exited_during_initialization.html.erb deleted file mode 100644 index 9b1389d4..00000000 --- a/lib/passenger/templates/app_exited_during_initialization.html.erb +++ /dev/null @@ -1,19 +0,0 @@ -<% layout 'error_layout', :title => "#{@app_name} application could not be started" do %> -

<%= @app_name %> application could not be started

-
- - The application has exited during startup (i.e. during the evaluation of - config/environment.rb). The error message may have been - written to the web server's log file. Please check the web server's - log file (i.e. not the (Rails) application's log file) to find out why the application - exited. - -
-
Application root:
-
- <%=h @app_root %> -
-
- -
-<% end %> diff --git a/lib/passenger/abstract_request_handler.rb b/lib/phusion_passenger/abstract_request_handler.rb similarity index 55% rename from lib/passenger/abstract_request_handler.rb rename to lib/phusion_passenger/abstract_request_handler.rb index d0addfdb..19197853 100644 --- a/lib/passenger/abstract_request_handler.rb +++ b/lib/phusion_passenger/abstract_request_handler.rb @@ -18,9 +18,9 @@ require 'socket' require 'fcntl' -require 'passenger/utils' -require 'passenger/native_support' -module Passenger +require 'phusion_passenger/message_channel' +require 'phusion_passenger/utils' +module PhusionPassenger # The request handler is the layer which connects Apache with the underlying application's # request dispatcher (i.e. either Rails's Dispatcher class or Rack). @@ -39,30 +39,20 @@ module Passenger # administrator maintenance overhead. These decisions are documented # in this section. # -# === Abstract namespace Unix sockets -# -# AbstractRequestHandler listens on a Unix socket for incoming requests. If possible, -# AbstractRequestHandler will try to create a Unix socket on the _abstract namespace_, -# instead of on the filesystem. If the RoR application crashes (segfault), -# or if it gets killed by SIGKILL, or if the system loses power, then there -# will be no stale socket files left on the filesystem. -# Unfortunately, abstract namespace Unix sockets are only supported by Linux. -# On systems that do not support abstract namespace Unix sockets, -# AbstractRequestHandler will automatically fallback to using regular Unix socket files. -# -# It is possible to force AbstractRequestHandler to use regular Unix socket files by -# setting the environment variable PASSENGER_NO_ABSTRACT_NAMESPACE_SOCKETS -# to 1. -# # === Owner pipes # # Because only the web server communicates directly with a request handler, # we want the request handler to exit if the web server has also exited. # This is implemented by using a so-called _owner pipe_. The writable part -# of the pipe will be owned by the web server. AbstractRequestHandler will -# continuously check whether the other side of the pipe has been closed. If -# so, then it knows that the web server has exited, and so the request handler -# will exit as well. This works even if the web server gets killed by SIGKILL. +# of the pipe will be passed to the web server* via a Unix socket, and the web +# server will own that part of the pipe, while AbstractRequestHandler owns +# the readable part of the pipe. AbstractRequestHandler will continuously +# check whether the other side of the pipe has been closed. If so, then it +# knows that the web server has exited, and so the request handler will exit +# as well. This works even if the web server gets killed by SIGKILL. +# +# * It might also be passed to the ApplicationPoolServerExecutable, if the web +# server's using ApplicationPoolServer instead of StandardApplicationPool. # # # == Request format @@ -94,7 +84,7 @@ class AbstractRequestHandler HARD_TERMINATION_SIGNAL = "SIGTERM" # Signal which will cause the Rails application to exit as soon as it's done processing a request. SOFT_TERMINATION_SIGNAL = "SIGUSR1" - BACKLOG_SIZE = 50 + BACKLOG_SIZE = 100 MAX_HEADER_SIZE = 128 * 1024 # String constants which exist to relieve Ruby's garbage collector. @@ -104,72 +94,117 @@ class AbstractRequestHandler CONTENT_LENGTH = 'CONTENT_LENGTH' # :nodoc: HTTP_CONTENT_LENGTH = 'HTTP_CONTENT_LENGTH' # :nodoc: X_POWERED_BY = 'X-Powered-By' # :nodoc: + REQUEST_METHOD = 'REQUEST_METHOD' # :nodoc: + PING = 'ping' # :nodoc: # The name of the socket on which the request handler accepts - # new connections. This is either a Unix socket filename, or - # the name for an abstract namespace Unix socket. + # new connections. At this moment, this value is always the filename + # of a Unix domain socket. # - # If +socket_name+ refers to an abstract namespace Unix socket, - # then the name does _not_ contain a leading null byte. - # - # See also using_abstract_namespace? + # See also #socket_type. attr_reader :socket_name - + + # The type of socket that #socket_name refers to. At the moment, the + # value is always 'unix', which indicates a Unix domain socket. + attr_reader :socket_type + + # Specifies the maximum allowed memory usage, in MB. If after having processed + # a request AbstractRequestHandler detects that memory usage has risen above + # this limit, then it will gracefully exit (that is, exit after having processed + # all pending requests). + # + # A value of 0 (the default) indicates that there's no limit. + attr_accessor :memory_limit + + # The number of times the main loop has iterated so far. Mostly useful + # for unit test assertions. + attr_reader :iterations + + # Number of requests processed so far. This includes requests that raised + # exceptions. + attr_reader :processed_requests + # Create a new RequestHandler with the given owner pipe. # +owner_pipe+ must be the readable part of a pipe IO object. - def initialize(owner_pipe) - if abstract_namespace_sockets_allowed? - @using_abstract_namespace = create_unix_socket_on_abstract_namespace - else - @using_abstract_namespace = false - end - if !@using_abstract_namespace + # + # Additionally, the following options may be given: + # - memory_limit: Used to set the +memory_limit+ attribute. + def initialize(owner_pipe, options = {}) + if should_use_unix_sockets? create_unix_socket_on_filesystem + else + create_tcp_socket end - @socket.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) + @socket.close_on_exec! @owner_pipe = owner_pipe @previous_signal_handlers = {} + @main_loop_thread_lock = Mutex.new + @main_loop_thread_cond = ConditionVariable.new + @memory_limit = options["memory_limit"] || 0 + @iterations = 0 + @processed_requests = 0 end # Clean up temporary stuff created by the request handler. - # This method should be called after the main loop has exited. + # + # If the main loop was started by #main_loop, then this method may only + # be called after the main loop has exited. + # + # If the main loop was started by #start_main_loop_thread, then this method + # may be called at any time, and it will stop the main loop thread. def cleanup + if @main_loop_thread + @main_loop_thread.raise(Interrupt.new("Cleaning up")) + @main_loop_thread.join + end @socket.close rescue nil @owner_pipe.close rescue nil - if !using_abstract_namespace? - File.unlink(@socket_name) rescue nil - end + File.unlink(@socket_name) rescue nil end - # Returns whether socket_name refers to an abstract namespace Unix socket. - def using_abstract_namespace? - return @using_abstract_namespace + # Check whether the main loop's currently running. + def main_loop_running? + return @main_loop_running end # Enter the request handler's main loop. def main_loop reset_signal_handlers begin - done = false - while !done + @graceful_termination_pipe = IO.pipe + @graceful_termination_pipe[0].close_on_exec! + @graceful_termination_pipe[1].close_on_exec! + + @main_loop_thread_lock.synchronize do + @main_loop_running = true + @main_loop_thread_cond.broadcast + end + + install_useful_signal_handlers + + while true + @iterations += 1 client = accept_connection if client.nil? break end - trap SOFT_TERMINATION_SIGNAL do - done = true - end begin headers, input = parse_request(client) if headers - process_request(headers, input, client) + if headers[REQUEST_METHOD] == PING + process_ping(headers, input, client) + else + process_request(headers, input, client) + end end rescue IOError, SocketError, SystemCallError => e print_exception("Passenger RequestHandler", e) ensure + # 'input' is the same as 'client' so we don't + # need to close that. client.close rescue nil end - trap SOFT_TERMINATION_SIGNAL, DEFAULT + @processed_requests += 1 end rescue EOFError # Exit main loop. @@ -181,47 +216,54 @@ def main_loop raise end ensure + @graceful_termination_pipe[0].close rescue nil + @graceful_termination_pipe[1].close rescue nil revert_signal_handlers + @main_loop_thread_lock.synchronize do + @main_loop_running = false + @main_loop_thread_cond.broadcast + end end end - -private - include Utils - - def create_unix_socket_on_abstract_namespace - while true - begin - # I have no idea why, but using base64-encoded IDs - # don't pass the unit tests. I couldn't find the cause - # of the problem. The system supports base64-encoded - # names for abstract namespace unix sockets just fine. - @socket_name = generate_random_id(:hex) - @socket_name = @socket_name.slice(0, NativeSupport::UNIX_PATH_MAX - 2) - fd = NativeSupport.create_unix_socket("\x00#{socket_name}", BACKLOG_SIZE) - @socket = IO.new(fd) - @socket.instance_eval do - def accept - fd = NativeSupport.accept(fileno) - return IO.new(fd) - end - end - return true - rescue Errno::EADDRINUSE - # Do nothing, try again with another name. - rescue Errno::ENOENT - # Abstract namespace sockets not supported on this system. - return false + + # Start the main loop in a new thread. This thread will be stopped by #cleanup. + def start_main_loop_thread + @main_loop_thread = Thread.new do + main_loop + end + @main_loop_thread_lock.synchronize do + while !@main_loop_running + @main_loop_thread_cond.wait(@main_loop_thread_lock) end end end + +private + include Utils + def should_use_unix_sockets? + # There seems to be a bug in MacOS X w.r.t. Unix sockets. + # When the Unix socket subsystem is under high stress, a + # recv()/read() on a Unix socket can return 0 even when EOF is + # not reached. We work around this by using TCP sockets on + # MacOS X. + return RUBY_PLATFORM !~ /darwin/ + end + def create_unix_socket_on_filesystem done = false while !done begin - @socket_name = "/tmp/passenger.#{generate_random_id(:base64)}" - @socket_name = @socket_name.slice(0, NativeSupport::UNIX_PATH_MAX - 1) + if defined?(NativeSupport) + unix_path_max = NativeSupport::UNIX_PATH_MAX + else + unix_path_max = 100 + end + @socket_name = "#{passenger_tmpdir}/passenger_backend.#{generate_random_id(:base64)}" + @socket_name = @socket_name.slice(0, unix_path_max - 1) @socket = UNIXServer.new(@socket_name) + @socket.listen(BACKLOG_SIZE) + @socket_type = "unix" File.chmod(0600, @socket_name) done = true rescue Errno::EADDRINUSE @@ -229,12 +271,21 @@ def create_unix_socket_on_filesystem end end end + + def create_tcp_socket + # We use "127.0.0.1" as address in order to force + # TCPv4 instead of TCPv6. + @socket = TCPServer.new('127.0.0.1', 0) + @socket.listen(BACKLOG_SIZE) + @socket_name = "127.0.0.1:#{@socket.addr[1]}" + @socket_type = "tcp" + end # Reset signal handlers to their default handler, and install some # special handlers for a few signals. The previous signal handlers # will be put back by calling revert_signal_handlers. def reset_signal_handlers - Signal.list.each_key do |signal| + Signal.list_trappable.each_key do |signal| begin prev_handler = trap(signal, DEFAULT) if prev_handler != DEFAULT @@ -245,9 +296,45 @@ def reset_signal_handlers end end trap('HUP', IGNORE) + end + + def install_useful_signal_handlers + trappable_signals = Signal.list_trappable + + trap(SOFT_TERMINATION_SIGNAL) do + @graceful_termination_pipe[1].close rescue nil + end if trappable_signals.has_key?(SOFT_TERMINATION_SIGNAL.sub(/^SIG/, '')) + trap('ABRT') do raise SignalException, "SIGABRT" - end + end if trappable_signals.has_key?('ABRT') + + trap('QUIT') do + if Kernel.respond_to?(:caller_for_all_threads) + output = "========== Process #{Process.pid}: backtrace dump ==========\n" + caller_for_all_threads.each_pair do |thread, stack| + output << ("-" * 60) << "\n" + output << "# Thread: #{thread.inspect}, " + if thread == Thread.main + output << "[main thread], " + else + output << "[current thread], " + end + output << "alive = #{thread.alive?}\n" + output << ("-" * 60) << "\n" + output << " " << stack.join("\n ") + output << "\n\n" + end + else + output = "========== Process #{Process.pid}: backtrace dump ==========\n" + output << ("-" * 60) << "\n" + output << "# Current thread: #{Thread.current.inspect}\n" + output << ("-" * 60) << "\n" + output << " " << caller.join("\n ") + end + STDERR.puts(output) + STDERR.flush + end if trappable_signals.has_key?('QUIT') end def revert_signal_handlers @@ -257,10 +344,10 @@ def revert_signal_handlers end def accept_connection - ios = select([@socket, @owner_pipe])[0] + ios = select([@socket, @owner_pipe, @graceful_termination_pipe[0]]).first if ios.include?(@socket) client = @socket.accept - client.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) + client.close_on_exec! # The real input stream is not seekable (calling _seek_ # or _rewind_ on it will raise an exception). But some @@ -274,8 +361,10 @@ def accept_connection return client else - # The other end of the pipe has been closed. - # So we know all owning processes have quit. + # The other end of the owner pipe has been closed, or the + # graceful termination pipe has been closed. This is our + # call to gracefully terminate (after having processed all + # incoming requests). return nil end end @@ -301,6 +390,10 @@ def parse_request(socket) print_exception("Passenger RequestHandler", e) end + def process_ping(env, input, output) + output.write("pong") + end + # Generate a long, cryptographically secure random ID string, which # is also a valid filename. def generate_random_id(method) @@ -318,11 +411,6 @@ def generate_random_id(method) return data end - def abstract_namespace_sockets_allowed? - return !ENV['PASSENGER_NO_ABSTRACT_NAMESPACE_SOCKETS'] || - ENV['PASSENGER_NO_ABSTRACT_NAMESPACE_SOCKETS'].empty? - end - def self.determine_passenger_version rakefile = "#{File.dirname(__FILE__)}/../../Rakefile" if File.exist?(rakefile) @@ -347,4 +435,4 @@ def self.determine_passenger_header PASSENGER_HEADER = determine_passenger_header end -end # module Passenger +end # module PhusionPassenger diff --git a/lib/passenger/abstract_server.rb b/lib/phusion_passenger/abstract_server.rb similarity index 92% rename from lib/passenger/abstract_server.rb rename to lib/phusion_passenger/abstract_server.rb index 0470f5b0..275b7c37 100644 --- a/lib/passenger/abstract_server.rb +++ b/lib/phusion_passenger/abstract_server.rb @@ -19,10 +19,9 @@ require 'socket' require 'set' require 'timeout' -require 'passenger/message_channel' -require 'passenger/utils' -require 'passenger/native_support' -module Passenger +require 'phusion_passenger/message_channel' +require 'phusion_passenger/utils' +module PhusionPassenger # An abstract base class for a server, with the following properties: # @@ -43,7 +42,7 @@ module Passenger # # Here's an example on using AbstractServer: # -# class MyServer < Passenger::AbstractServer +# class MyServer < PhusionPassenger::AbstractServer # def initialize # super() # define_message_handler(:hello, :handle_hello) @@ -88,11 +87,25 @@ class ServerNotStarted < StandardError class ServerError < StandardError end + # The last time when this AbstractServer had processed a message. + attr_accessor :last_activity_time + + # The maximum time that this AbstractServer may be idle. Used by + # AbstractServerCollection to determine when this object should + # be cleaned up. nil or 0 indicate that this object should never + # be idle cleaned. + attr_accessor :max_idle_time + + # Used by AbstractServerCollection to remember when this AbstractServer + # should be idle cleaned. + attr_accessor :next_cleaning_time + def initialize @done = false @message_handlers = {} @signal_handlers = {} @orig_signal_handlers = {} + @last_activity_time = Time.now end # Start the server. This method does not block since the server runs @@ -109,7 +122,8 @@ def start @parent_socket, @child_socket = UNIXSocket.pair before_fork - @pid = fork do + @pid = fork + if @pid.nil? begin STDOUT.sync = true STDERR.sync = true @@ -278,7 +292,7 @@ def quit_main # Reset all signal handlers to default. This is called in the child process, # before entering the main loop. def reset_signal_handlers - Signal.list.each_key do |signal| + Signal.list_trappable.each_key do |signal| begin @orig_signal_handlers[signal] = trap(signal, 'DEFAULT') rescue ArgumentError @@ -311,6 +325,7 @@ def main_loop while !@done begin name, *args = channel.read + @last_activity_time = Time.now if name.nil? @done = true elsif @message_handlers.has_key?(name) @@ -331,4 +346,4 @@ def main_loop end end -end # module Passenger +end # module PhusionPassenger diff --git a/lib/phusion_passenger/abstract_server_collection.rb b/lib/phusion_passenger/abstract_server_collection.rb new file mode 100644 index 00000000..32a73bfb --- /dev/null +++ b/lib/phusion_passenger/abstract_server_collection.rb @@ -0,0 +1,301 @@ +# Phusion Passenger - http://www.modrails.com/ +# Copyright (C) 2008 Phusion +# +# Phusion Passenger is a trademark of Hongli Lai & Ninh Bui. +# +# 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 'phusion_passenger/utils' + +module PhusionPassenger + +# This class maintains a collection of AbstractServer objects. One can add new +# AbstractServer objects, or look up existing ones via a key. +# AbstractServerCollection also automatically takes care of cleaning up +# AbstractServers that have been idle for too long. +# +# This class exists because both SpawnManager and Railz::FrameworkSpawner need this kind +# of functionality. SpawnManager maintains a collection of Railz::FrameworkSpawner +# and Railz::ApplicationSpawner objects, while Railz::FrameworkSpawner maintains a +# collection of Railz::ApplicationSpawner objects. +# +# This class is thread-safe as long as the specified thread-safety rules are followed. +class AbstractServerCollection + attr_reader :next_cleaning_time + + include Utils + + def initialize + @collection = {} + @lock = Mutex.new + @cleanup_lock = Mutex.new + @cond = ConditionVariable.new + @done = false + + # The next time the cleaner thread should check for idle servers. + # The value may be nil, in which case the value will be calculated + # at the end of the #synchronized block. + # + # Invariant: + # if value is not nil: + # There exists an s in @collection with s.next_cleaning_time == value. + # for all s in @collection: + # if eligable_for_cleanup?(s): + # s.next_cleaning_time <= value + @next_cleaning_time = Time.now + 60 * 60 + @next_cleaning_time_changed = false + + @cleaner_thread = Thread.new do + begin + @lock.synchronize do + cleaner_thread_main + end + rescue Exception => e + print_exception(self.class.to_s, e) + end + end + end + + # Acquire the lock for this AbstractServerCollection object, and run + # the code within the block. The entire block will be a single atomic + # operation. + def synchronize + @lock.synchronize do + yield + if @next_cleaning_time.nil? + @collection.each_value do |server| + if @next_cleaning_time.nil? || + (eligable_for_cleanup?(server) && + server.next_cleaning_time < @next_cleaning_time + ) + @next_cleaning_time = server.next_cleaning_time + end + end + if @next_cleaning_time.nil? + # There are no servers in the collection with an idle timeout. + @next_cleaning_time = Time.now + 60 * 60 + end + @next_cleaning_time_changed = true + end + if @next_cleaning_time_changed + @next_cleaning_time_changed = false + @cond.signal + end + end + end + + # Lookup and returns an AbstractServer with the given key. + # + # If there is no AbstractSerer associated with the given key, then the given + # block will be called. That block must return an AbstractServer object. Then, + # that object will be stored in the collection, and returned. + # + # The block must set the 'max_idle_time' attribute on the AbstractServer. + # AbstractServerCollection's idle cleaning interval will be adapted to accomodate + # with this. Changing the value outside this block is not guaranteed to have any + # effect on the idle cleaning interval. + # A max_idle_time value of nil or 0 means the AbstractServer will never be idle cleaned. + # + # If the block raises an exception, then the collection will not be modified, + # and the exception will be propagated. + # + # Precondition: this method must be called within a #synchronize block. + def lookup_or_add(key) + raise ArgumentError, "cleanup() has already been called." if @done + server = @collection[key] + if server + register_activity(server) + return server + else + server = yield + if !server.respond_to?(:start) + raise TypeError, "The block didn't return a valid AbstractServer object." + end + if eligable_for_cleanup?(server) + server.next_cleaning_time = Time.now + server.max_idle_time + if @next_cleaning_time && server.next_cleaning_time < @next_cleaning_time + @next_cleaning_time = server.next_cleaning_time + @next_cleaning_time_changed = true + end + end + @collection[key] = server + return server + end + end + + # Checks whether there's an AbstractServer object associated with the given key. + # + # Precondition: this method must be called within a #synchronize block. + def has_key?(key) + return @collection.has_key?(key) + end + + # Checks whether the collection is empty. + # + # Precondition: this method must be called within a #synchronize block. + def empty? + return @collection.empty? + end + + # Deletes from the collection the AbstractServer that's associated with the + # given key. If no such AbstractServer exists, nothing will happen. + # + # If the AbstractServer is started, then it will be stopped before deletion. + # + # Precondition: this method must be called within a #synchronize block. + def delete(key) + raise ArgumentError, "cleanup() has already been called." if @done + server = @collection[key] + if server + if server.started? + server.stop + end + @collection.delete(key) + if server.next_cleaning_time == @next_cleaning_time + @next_cleaning_time = nil + end + end + end + + # Notify this AbstractServerCollection that +server+ has performed an activity. + # This AbstractServerCollection will update the idle information associated with +server+ + # accordingly. + # + # lookup_or_add already automatically updates idle information, so you only need to + # call this method if the time at which the server has performed an activity is + # not close to the time at which lookup_or_add had been called. + # + # Precondition: this method must be called within a #synchronize block. + def register_activity(server) + if eligable_for_cleanup?(server) + if server.next_cleaning_time == @next_cleaning_time + @next_cleaning_time = nil + end + server.next_cleaning_time = Time.now + server.max_idle_time + end + end + + # Tell the cleaner thread to check the collection as soon as possible, instead + # of sleeping until the next scheduled cleaning time. + # + # Precondition: this method must NOT be called within a #synchronize block. + def check_idle_servers! + @lock.synchronize do + @next_cleaning_time = Time.now - 60 * 60 + @cond.signal + end + end + + # Iterate over all AbstractServer objects. + # + # Precondition: this method must be called within a #synchronize block. + def each + each_pair do |key, server| + yield server + end + end + + # Iterate over all keys and associated AbstractServer objects. + # + # Precondition: this method must be called within a #synchronize block. + def each_pair + raise ArgumentError, "cleanup() has already been called." if @done + @collection.each_pair do |key, server| + yield(key, server) + end + end + + # Delete all AbstractServers from the collection. Each AbstractServer will be + # stopped, if necessary. + # + # Precondition: this method must be called within a #synchronize block. + def clear + @collection.each_value do |server| + if server.started? + server.stop + end + end + @collection.clear + @next_cleaning_time = nil + end + + # Cleanup all resources used by this AbstractServerCollection. All AbstractServers + # from the collection will be deleted. Each AbstractServer will be stopped, if + # necessary. The background thread which removes idle AbstractServers will be stopped. + # + # After calling this method, this AbstractServerCollection object will become + # unusable. + # + # Precondition: this method must *NOT* be called within a #synchronize block. + def cleanup + @cleanup_lock.synchronize do + return if @done + @lock.synchronize do + @done = true + @cond.signal + end + @cleaner_thread.join + clear + end + end + +private + def cleaner_thread_main + while !@done + current_time = Time.now + # We add a 0.2 seconds delay to the sleep time because system + # timers are not entirely accurate. + sleep_time = (@next_cleaning_time - current_time).to_f + 0.2 + if sleep_time > 0 && @cond.timed_wait(@lock, sleep_time) + next + else + keys_to_delete = nil + @next_cleaning_time = nil + @collection.each_pair do |key, server| + if eligable_for_cleanup?(server) + # Cleanup this server if its idle timeout has expired. + if server.next_cleaning_time <= current_time + keys_to_delete ||= [] + keys_to_delete << key + if server.started? + server.stop + end + # If not, then calculate the next cleaning time because + # we're iterating the collection anyway. + elsif @next_cleaning_time.nil? || + server.next_cleaning_time < @next_cleaning_time + @next_cleaning_time = server.next_cleaning_time + end + end + end + if keys_to_delete + keys_to_delete.each do |key| + @collection.delete(key) + end + end + if @next_cleaning_time.nil? + # There are no servers in the collection with an idle timeout. + @next_cleaning_time = Time.now + 60 * 60 + end + end + end + end + + # Checks whether the given server is eligible for being idle cleaned. + def eligable_for_cleanup?(server) + return server.max_idle_time && server.max_idle_time != 0 + end +end + +end # module PhusionPassenger diff --git a/lib/phusion_passenger/admin_tools.rb b/lib/phusion_passenger/admin_tools.rb new file mode 100644 index 00000000..a137e4f9 --- /dev/null +++ b/lib/phusion_passenger/admin_tools.rb @@ -0,0 +1,25 @@ +module PhusionPassenger + +module AdminTools + def self.tmpdir + ["PASSENGER_TMPDIR", "TMPDIR"].each do |name| + if ENV.has_key?(name) && !ENV[name].empty? + return ENV[name] + end + end + return "/tmp" + end + + def self.process_is_alive?(pid) + begin + Process.kill(0, pid) + return true + rescue Errno::ESRCH + return false + rescue SystemCallError => e + return true + end + end +end # module AdminTools + +end # module PhusionPassenger diff --git a/lib/phusion_passenger/admin_tools/control_process.rb b/lib/phusion_passenger/admin_tools/control_process.rb new file mode 100644 index 00000000..70e74c63 --- /dev/null +++ b/lib/phusion_passenger/admin_tools/control_process.rb @@ -0,0 +1,107 @@ +require 'rexml/document' +require 'fileutils' +require 'phusion_passenger/admin_tools' +require 'phusion_passenger/message_channel' + +module PhusionPassenger +module AdminTools + +class ControlProcess + class Instance + attr_accessor :pid, :socket_name, :socket_type, :sessions, :uptime + INT_PROPERTIES = [:pid, :sessions] + end + + attr_accessor :path + attr_accessor :pid + + def self.list(clean_stale = true) + results = [] + Dir["#{AdminTools.tmpdir}/passenger.*"].each do |dir| + dir =~ /passenger.(\d+)\Z/ + next if !$1 + pid = $1.to_i + begin + results << ControlProcess.new(pid, dir) + rescue ArgumentError + # Stale Passenger temp folder. Clean it up if instructed. + if clean_stale + puts "*** Cleaning stale folder #{dir}" + FileUtils.rm_rf(dir) + end + end + end + return results + end + + def initialize(pid, path = nil) + if !AdminTools.process_is_alive?(pid) + raise ArgumentError, "There is no control process with PID #{pid}." + end + @pid = pid + if path + @path = path + else + @path = "#{AdminTools.tmpdir}/passenger.#{pid}" + end + end + + def status + reload + return @status + end + + def xml + reload + return @xml + end + + def domains + reload + return @domains + end + + def instances + return domains.map do |domain| + domain[:instances] + end.flatten + end + +private + def reload + return if @status + File.open("#{path}/status.fifo", 'r') do |f| + channel = MessageChannel.new(f) + @status = channel.read_scalar + @xml = channel.read_scalar + end + doc = REXML::Document.new(@xml) + + @domains = [] + doc.elements.each("info/domains/domain") do |domain| + instances = [] + d = { + :name => domain.elements["name"].text, + :instances => instances + } + domain.elements.each("instances/instance") do |instance| + i = Instance.new + instance.elements.each do |element| + if i.respond_to?("#{element.name}=") + if Instance::INT_PROPERTIES.include?(element.name.to_sym) + value = element.text.to_i + else + value = element.text + end + i.send("#{element.name}=", value) + end + end + instances << i + end + @domains << d + end + end +end + +end # module AdminTools +end # module PhusionPassenger diff --git a/lib/passenger/application.rb b/lib/phusion_passenger/application.rb similarity index 84% rename from lib/passenger/application.rb rename to lib/phusion_passenger/application.rb index cc24f3df..b118d2e2 100644 --- a/lib/passenger/application.rb +++ b/lib/phusion_passenger/application.rb @@ -17,8 +17,8 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. require 'rubygems' -require 'passenger/exceptions' -module Passenger +require 'phusion_passenger/exceptions' +module PhusionPassenger # Represents a single application instance. class Application @@ -29,10 +29,16 @@ class Application # The process ID of this application instance. attr_reader :pid - # The name of the Unix socket on which the application instance will accept - # new connections. + # The name of the socket on which the application instance will accept + # new connections. See #listen_socket_type on how one should interpret + # this value. attr_reader :listen_socket_name + # The type of socket that #listen_socket_name refers to. Currently this + # is always 'unix', which means that #listen_socket_name refers to the + # filename of a Unix domain socket. + attr_reader :listen_socket_type + # The owner pipe of the application instance (an IO object). Please see # RequestHandler for a description of the owner pipe. attr_reader :owner_pipe @@ -81,23 +87,14 @@ def self.detect_framework_version(app_root) # Creates a new instance of Application. The parameters correspond with the attributes # of the same names. No exceptions will be thrown. - def initialize(app_root, pid, listen_socket_name, using_abstract_namespace, owner_pipe) + def initialize(app_root, pid, listen_socket_name, listen_socket_type, owner_pipe) @app_root = app_root @pid = pid @listen_socket_name = listen_socket_name - @using_abstract_namespace = using_abstract_namespace + @listen_socket_type = listen_socket_type @owner_pipe = owner_pipe end - # Whether _listen_socket_name_ refers to a Unix socket in the abstract namespace. - # In any case, _listen_socket_name_ does *not* contain the leading null byte. - # - # Note that at the moment, only Linux seems to support abstract namespace Unix - # sockets. - def using_abstract_namespace? - return @using_abstract_namespace - end - # Close the connection with the application instance. If there are no other # processes that have connections to this application instance, then it will # shutdown as soon as possible. @@ -108,4 +105,4 @@ def close end end -end # module Passenger +end # module PhusionPassenger diff --git a/lib/passenger/console_text_template.rb b/lib/phusion_passenger/console_text_template.rb similarity index 97% rename from lib/passenger/console_text_template.rb rename to lib/phusion_passenger/console_text_template.rb index 32006da0..0150c729 100644 --- a/lib/passenger/console_text_template.rb +++ b/lib/phusion_passenger/console_text_template.rb @@ -17,7 +17,7 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. require 'erb' -module Passenger +module PhusionPassenger class ConsoleTextTemplate TEMPLATE_DIR = "#{File.dirname(__FILE__)}/templates" @@ -58,4 +58,4 @@ def substitute_color_tags(data) end end -end # module Passenger +end # module PhusionPassenger diff --git a/lib/passenger/constants.rb b/lib/phusion_passenger/constants.rb similarity index 73% rename from lib/passenger/constants.rb rename to lib/phusion_passenger/constants.rb index 47aa7d5e..50030812 100644 --- a/lib/passenger/constants.rb +++ b/lib/phusion_passenger/constants.rb @@ -14,13 +14,7 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -module Passenger - FRAMEWORK_SPAWNER_MAX_IDLE_TIME = 30 * 60 - APP_SPAWNER_MAX_IDLE_TIME = 10 * 60 - - SPAWNER_CLEAN_INTERVAL = [ - FRAMEWORK_SPAWNER_MAX_IDLE_TIME, - APP_SPAWNER_MAX_IDLE_TIME - ].min + 5 - APP_SPAWNER_CLEAN_INTERVAL = APP_SPAWNER_MAX_IDLE_TIME + 5 +module PhusionPassenger + DEFAULT_FRAMEWORK_SPAWNER_MAX_IDLE_TIME = 30 * 60 + DEFAULT_APP_SPAWNER_MAX_IDLE_TIME = 10 * 60 end diff --git a/lib/passenger/dependencies.rb b/lib/phusion_passenger/dependencies.rb similarity index 87% rename from lib/passenger/dependencies.rb rename to lib/phusion_passenger/dependencies.rb index 0b730620..d00ea605 100644 --- a/lib/passenger/dependencies.rb +++ b/lib/phusion_passenger/dependencies.rb @@ -16,8 +16,8 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -require 'passenger/platform_info' -module Passenger +require 'phusion_passenger/platform_info' +module PhusionPassenger # Represents a dependency software that Passenger requires. It's used by the # installer to check whether all dependencies are available. A Dependency object @@ -53,11 +53,11 @@ def check private class Result - def found(filename_or_boolean = nil) - if filename_or_boolean.nil? + def found(*args) + if args.empty? @found = true else - @found = filename_or_boolean + @found = args.first end end @@ -90,8 +90,12 @@ def call_init_block # Namespace which contains the different dependencies that Passenger may require. # See Dependency for more information. module Dependencies # :nodoc: all - include PlatformInfo - + # Returns whether fastthread is a required dependency for the current + # Ruby interpreter. + def self.fastthread_required? + return (!defined?(RUBY_ENGINE) || RUBY_ENGINE == "ruby") && RUBY_VERSION < "1.8.7" + end + GCC = Dependency.new do |dep| dep.name = "GNU C++ compiler" dep.define_checker do |result| @@ -103,7 +107,7 @@ module Dependencies # :nodoc: all end end if RUBY_PLATFORM =~ /linux/ - case LINUX_DISTRO + case PlatformInfo.linux_distro when :ubuntu, :debian dep.install_command = "apt-get install build-essential" when :rhel, :fedora, :centos @@ -123,13 +127,14 @@ module Dependencies # :nodoc: all require 'rbconfig' begin require 'mkmf' - result.found(File.exist?(Config::CONFIG['archdir'] + "/ruby.h")) + header_dir = Config::CONFIG['rubyhdrdir'] || Config::CONFIG['archdir'] + result.found(File.exist?("#{header_dir}/ruby.h")) rescue LoadError result.not_found end end if RUBY_PLATFORM =~ /linux/ - case LINUX_DISTRO + case PlatformInfo.linux_distro when :ubuntu, :debian dep.install_command = "apt-get install ruby1.8-dev" when :rhel, :fedora, :centos @@ -155,7 +160,7 @@ module Dependencies # :nodoc: all end end if RUBY_PLATFORM =~ /linux/ - case LINUX_DISTRO + case PlatformInfo.linux_distro when :ubuntu, :debian dep.install_command = "apt-get install libopenssl-ruby" end @@ -185,17 +190,11 @@ module Dependencies # :nodoc: all Rake = Dependency.new do |dep| dep.name = "Rake" dep.define_checker do |result| - bindir = File.dirname(PlatformInfo::RUBY) - rake = File.join(bindir, "rake") + rake = PlatformInfo::RAKE if File.executable?(rake) result.found(rake) else - rake = PlatformInfo.find_command("rake") - if rake.nil? - result.not_found - else - result.found(rake) - end + result.not_found end end dep.website = "http://rake.rubyforge.org/" @@ -205,14 +204,14 @@ module Dependencies # :nodoc: all Apache2 = Dependency.new do |dep| dep.name = "Apache 2" dep.define_checker do |result| - if HTTPD.nil? + if PlatformInfo.httpd.nil? result.not_found else - result.found(HTTPD) + result.found(PlatformInfo.httpd) end end if RUBY_PLATFORM =~ /linux/ - case LINUX_DISTRO + case PlatformInfo.linux_distro when :ubuntu, :debian dep.install_command = "apt-get install apache2-mpm-prefork" when :rhel, :fedora, :centos @@ -230,14 +229,14 @@ module Dependencies # :nodoc: all Apache2_DevHeaders = Dependency.new do |dep| dep.name = "Apache 2 development headers" dep.define_checker do |result| - if APXS2.nil? + if PlatformInfo.apxs2.nil? result.not_found else - result.found(APXS2) + result.found(PlatformInfo.apxs2) end end if RUBY_PLATFORM =~ /linux/ - case LINUX_DISTRO + case PlatformInfo.linux_distro when :ubuntu, :debian dep.install_command = "apt-get install apache2-prefork-dev" dep.provides = [Apache2] @@ -257,10 +256,14 @@ module Dependencies # :nodoc: all APR_DevHeaders = Dependency.new do |dep| dep.name = "Apache Portable Runtime (APR) development headers" dep.define_checker do |result| - result.found(APR_CONFIG) + if PlatformInfo.apr_config.nil? + result.not_found + else + result.found(PlatformInfo.apr_config) + end end if RUBY_PLATFORM =~ /linux/ - case LINUX_DISTRO + case PlatformInfo.linux_distro when :ubuntu, :debian dep.install_command = "apt-get install libapr1-dev" when :rhel, :fedora, :centos @@ -278,9 +281,13 @@ module Dependencies # :nodoc: all end APU_DevHeaders = Dependency.new do |dep| - dep.name = "Apache Portable Runtime Utility (APR) development headers" + dep.name = "Apache Portable Runtime Utility (APU) development headers" dep.define_checker do |result| - result.found(APU_CONFIG) + if PlatformInfo.apu_config.nil? + result.not_found + else + result.found(PlatformInfo.apu_config) + end end dep.website = "http://httpd.apache.org/" dep.website_comments = "APR Utility is an integrated part of Apache." @@ -321,4 +328,4 @@ module Dependencies # :nodoc: all end end -end # module Passenger +end # module PhusionPassenger diff --git a/lib/phusion_passenger/events.rb b/lib/phusion_passenger/events.rb new file mode 100644 index 00000000..33c090e7 --- /dev/null +++ b/lib/phusion_passenger/events.rb @@ -0,0 +1,45 @@ +# Phusion Passenger - http://www.modrails.com/ +# Copyright (C) 2009 Phusion +# +# Phusion Passenger is a trademark of Hongli Lai & Ninh Bui. +# +# 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. + +module PhusionPassenger + @@event_starting_worker_process = [] + @@event_stopping_worker_process = [] + + def self.on_event(name, &block) + callback_list_for_event(name) << block + end + + def self.call_event(name, *args) + callback_list_for_event(name).each do |callback| + callback.call(*args) + end + end + +private + def self.callback_list_for_event(name) + return case name + when :starting_worker_process + @@event_starting_worker_process + when :stopping_worker_process + @@event_stopping_worker_process + else + raise ArgumentError, "Unknown event name '#{name}'" + end + end + +end # module PhusionPassenger diff --git a/lib/passenger/exceptions.rb b/lib/phusion_passenger/exceptions.rb similarity index 88% rename from lib/passenger/exceptions.rb rename to lib/phusion_passenger/exceptions.rb index 69a7946b..a57ed7ed 100644 --- a/lib/passenger/exceptions.rb +++ b/lib/phusion_passenger/exceptions.rb @@ -16,7 +16,7 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -module Passenger +module PhusionPassenger # Indicates that there is no Ruby on Rails version installed that satisfies # a given Ruby on Rails Gem version specification. @@ -50,14 +50,18 @@ def initialize(message, child_exception = nil) # Railz::FrameworkSpawner or SpawnManager was unable to spawn an 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. +# If the application called exit, then +child_exception+ is an instance of +# +SystemExit+. class AppInitError < InitializationError + # The application type, e.g. "rails" or "rack". attr_accessor :app_type + # Any messages printed to stderr before the failure. May be nil. + attr_accessor :stderr - def initialize(message, child_exception = nil, app_type = "rails") + def initialize(message, child_exception = nil, app_type = "rails", stderr = nil) super(message, child_exception) @app_type = app_type + @stderr = stderr end end @@ -88,4 +92,7 @@ def initialize(message, class_name, backtrace) end end -end # module Passenger +class InvalidPath < StandardError +end + +end # module PhusionPassenger diff --git a/lib/passenger/html_template.rb b/lib/phusion_passenger/html_template.rb similarity index 98% rename from lib/passenger/html_template.rb rename to lib/phusion_passenger/html_template.rb index 2531f1d6..fe0b9dab 100644 --- a/lib/passenger/html_template.rb +++ b/lib/phusion_passenger/html_template.rb @@ -18,7 +18,7 @@ require 'erb' -module Passenger +module PhusionPassenger # A convenience utility class for rendering our error pages. class HTMLTemplate @@ -101,4 +101,4 @@ def starts_with(str, substr) end end -end # module Passenger +end # module PhusionPassenger diff --git a/lib/passenger/message_channel.rb b/lib/phusion_passenger/message_channel.rb similarity index 97% rename from lib/passenger/message_channel.rb rename to lib/phusion_passenger/message_channel.rb index 6bee1a49..e36f9418 100644 --- a/lib/passenger/message_channel.rb +++ b/lib/phusion_passenger/message_channel.rb @@ -16,7 +16,7 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -module Passenger +module PhusionPassenger # This class provides convenience methods for: # - sending and receiving raw data over an IO channel. @@ -140,6 +140,7 @@ def read_scalar(max_size = nil) end buffer = '' while buffer.size < size + temp = '' # JRuby doesn't clear the buffer. TODO: remove this when JRuby has been fixed. buffer << @io.readpartial(size - buffer.size, temp) end return buffer @@ -219,4 +220,4 @@ def check_argument(arg) end end -end # module Passenger +end # module PhusionPassenger diff --git a/lib/phusion_passenger/platform_info.rb b/lib/phusion_passenger/platform_info.rb new file mode 100644 index 00000000..e537a5c4 --- /dev/null +++ b/lib/phusion_passenger/platform_info.rb @@ -0,0 +1,453 @@ +# Phusion Passenger - http://www.modrails.com/ +# Copyright (C) 2008, 2009 Phusion +# +# Phusion Passenger is a trademark of Hongli Lai & Ninh Bui. +# +# 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 'rbconfig' + +# Wow, I can't believe in how many ways one can build Apache in OS +# X! We have to resort to all sorts of tricks to make Passenger build +# out of the box on OS X. :-( +# +# In the name of usability and the "end user is the king" line of thought, +# I shall suffer the horrible faith of writing tons of autodetection code! + +# This module autodetects various platform-specific information, and +# provides that information through constants. +# +# Users can change the detection behavior by setting the environment variable +# APXS2 to the correct 'apxs' (or 'apxs2') binary, as provided by +# Apache. +module PlatformInfo +private + # Turn the specified class method into a memoized one. If the given + # class method is called without arguments, then its result will be + # memoized, frozen, and returned upon subsequent calls without arguments. + # Calls with arguments are never memoized. + # + # def self.foo(max = 10) + # return rand(max) + # end + # memoize :foo + # + # foo # => 3 + # foo # => 3 + # foo(100) # => 49 + # foo(100) # => 26 + # foo # => 3 + def self.memoize(method) + metaclass = class << self; self; end + metaclass.send(:alias_method, "_unmemoized_#{method}", method) + variable_name = "@@memoized_#{method}".sub(/\?/, '') + check_variable_name = "@@has_memoized_#{method}".sub(/\?/, '') + eval("#{variable_name} = nil") + eval("#{check_variable_name} = false") + source = %Q{ + def self.#{method}(*args) # def self.httpd(*args) + if args.empty? # if args.empty? + if !#{check_variable_name} # if !@@has_memoized_httpd + #{variable_name} = _unmemoized_#{method}.freeze # @@memoized_httpd = _unmemoized_httpd.freeze + #{check_variable_name} = true # @@has_memoized_httpd = true + end # end + return #{variable_name} # return @@memoized_httpd + else # else + return _unmemoized_#{method}(*args) # return _unmemoized_httpd(*args) + end # end + end # end + } + class_eval(source) + end + + def self.env_defined?(name) + return !ENV[name].nil? && !ENV[name].empty? + end + + def self.locate_ruby_executable(name) + if RUBY_PLATFORM =~ /darwin/ && + RUBY =~ %r(\A/System/Library/Frameworks/Ruby.framework/Versions/.*?/usr/bin/ruby\Z) + # On OS X we must look for Ruby binaries in /usr/bin. + # RubyGems puts executables (e.g. 'rake') in there, not in + # /System/Libraries/(...)/bin. + return "/usr/bin/#{name}" + else + return File.dirname(RUBY) + "/#{name}" + end + end + + # Look in the directory +dir+ and check whether there's an executable + # whose base name is equal to one of the elements in +possible_names+. + # If so, returns the full filename. If not, returns nil. + def self.select_executable(dir, *possible_names) + possible_names.each do |name| + filename = "#{dir}/#{name}" + if File.file?(filename) && File.executable?(filename) + return filename + end + end + return nil + end + + def self.find_apache2_executable(*possible_names) + [apache2_bindir, apache2_sbindir].each do |bindir| + if bindir.nil? + next + end + possible_names.each do |name| + filename = "#{bindir}/#{name}" + if File.file?(filename) && File.executable?(filename) + return filename + end + end + end + return nil + end + + def self.determine_apr_info + if apr_config.nil? + return [nil, nil] + else + flags = `#{apr_config} --cppflags --includes`.strip + libs = `#{apr_config} --link-ld`.strip + flags.gsub!(/-O\d? /, '') + if RUBY_PLATFORM =~ /solaris/ + # Remove flags not supported by GCC + flags = flags.split(/ +/).reject{ |f| f =~ /^\-mt/ }.join(' ') + end + return [flags, libs] + end + end + memoize :determine_apr_info + + def self.determine_apu_info + if apu_config.nil? + return [nil, nil] + else + flags = `#{apu_config} --includes`.strip + libs = `#{apu_config} --link-ld`.strip + flags.gsub!(/-O\d? /, '') + return [flags, libs] + end + end + memoize :determine_apu_info + + def self.read_file(filename) + return File.read(filename) + rescue + return "" + end + +public + # The absolute path to the current Ruby interpreter. + RUBY = Config::CONFIG['bindir'] + '/' + Config::CONFIG['RUBY_INSTALL_NAME'] + Config::CONFIG['EXEEXT'] + # The correct 'gem' and 'rake' commands for this Ruby interpreter. + GEM = locate_ruby_executable('gem') + RAKE = locate_ruby_executable('rake') + + # Check whether the specified command is in $PATH, and return its + # absolute filename. Returns nil if the command is not found. + # + # This function exists because system('which') doesn't always behave + # correctly, for some weird reason. + def self.find_command(name) + ENV['PATH'].split(File::PATH_SEPARATOR).detect do |directory| + path = File.join(directory, name.to_s) + if File.executable?(path) + return path + end + end + return nil + end + + + ################ Programs ################ + + + # The absolute path to the 'apxs' or 'apxs2' executable, or nil if not found. + def self.apxs2 + if env_defined?("APXS2") + return ENV["APXS2"] + end + ['apxs2', 'apxs'].each do |name| + command = find_command(name) + if !command.nil? + return command + end + end + return nil + end + memoize :apxs2 + + # The absolute path to the 'apachectl' or 'apache2ctl' binary. + def self.apache2ctl + return find_apache2_executable('apache2ctl', 'apachectl') + end + memoize :apache2ctl + + # The absolute path to the Apache binary (that is, 'httpd', 'httpd2', 'apache' or 'apache2'). + def self.httpd + if env_defined?('HTTPD') + return ENV['HTTPD'] + elsif apxs2.nil? + ["apache2", "httpd2", "apache", "httpd"].each do |name| + command = find_command(name) + if !command.nil? + return command + end + end + return nil + else + return find_apache2_executable(`#{apxs2} -q TARGET`.strip) + end + end + memoize :httpd + + # The absolute path to the 'apr-config' or 'apr-1-config' executable. + def self.apr_config + if env_defined?('APR_CONFIG') + return ENV['APR_CONFIG'] + elsif apxs2.nil? + return nil + else + filename = `#{apxs2} -q APR_CONFIG 2>/dev/null`.strip + if filename.empty? + apr_bindir = `#{apxs2} -q APR_BINDIR 2>/dev/null`.strip + if apr_bindir.empty? + return nil + else + return select_executable(apr_bindir, + "apr-1-config", "apr-config") + end + elsif File.exist?(filename) + return filename + else + return nil + end + end + end + memoize :apr_config + + # The absolute path to the 'apu-config' or 'apu-1-config' executable. + def self.apu_config + if env_defined?('APU_CONFIG') + return ENV['APU_CONFIG'] + elsif apxs2.nil? + return nil + else + filename = `#{apxs2} -q APU_CONFIG 2>/dev/null`.strip + if filename.empty? + apu_bindir = `#{apxs2} -q APU_BINDIR 2>/dev/null`.strip + if apu_bindir.empty? + return nil + else + return select_executable(apu_bindir, + "apu-1-config", "apu-config") + end + elsif File.exist?(filename) + return filename + else + return nil + end + end + end + memoize :apu_config + + + ################ Directories ################ + + + # The absolute path to the Apache 2 'bin' directory. + def self.apache2_bindir + if apxs2.nil? + return nil + else + return `#{apxs2} -q BINDIR 2>/dev/null`.strip + end + end + memoize :apache2_bindir + + # The absolute path to the Apache 2 'sbin' directory. + def self.apache2_sbindir + if apxs2.nil? + return nil + else + return `#{apxs2} -q SBINDIR`.strip + end + end + memoize :apache2_sbindir + + + ################ Compiler and linker flags ################ + + + # Compiler flags that should be used for compiling every C/C++ program, + # for portability reasons. These flags should be specified as last + # when invoking the compiler. + def self.portability_cflags + # _GLIBCPP__PTHREADS is for fixing Boost compilation on OpenBSD. + flags = ["-D_REENTRANT -D_GLIBCPP__PTHREADS -I/usr/local/include"] + if RUBY_PLATFORM =~ /solaris/ + flags << '-D_XOPEN_SOURCE=500 -D_XPG4_2 -D__EXTENSIONS__ -D__SOLARIS__' + flags << '-DBOOST_HAS_STDINT_H' unless RUBY_PLATFORM =~ /solaris2.9/ + flags << '-D__SOLARIS9__ -DBOOST__STDC_CONSTANT_MACROS_DEFINED' if RUBY_PLATFORM =~ /solaris2.9/ + flags << '-mcpu=ultrasparc' if RUBY_PLATFORM =~ /sparc/ + end + return flags.compact.join(" ").strip + end + memoize :portability_cflags + + # Linker flags that should be used for linking every C/C++ program, + # for portability reasons. These flags should be specified as last + # when invoking the linker. + def self.portability_ldflags + if RUBY_PLATFORM =~ /solaris/ + return '-lxnet -lrt -lsocket -lnsl -lpthread' + else + return '-lpthread' + end + end + memoize :portability_ldflags + + # The C compiler flags that are necessary to compile an Apache module. + # Includes portability_cflags. + def self.apache2_module_cflags(with_apr_flags = true) + flags = ["-fPIC"] + if with_apr_flags + flags << apr_flags + flags << apu_flags + end + if !apxs2.nil? + apxs2_flags = `#{apxs2} -q CFLAGS`.strip << " -I" << `#{apxs2} -q INCLUDEDIR`.strip + apxs2_flags.gsub!(/-O\d? /, '') + + # Remove flags not supported by GCC + if RUBY_PLATFORM =~ /solaris/ # TODO: Add support for people using SunStudio + # The big problem is Coolstack apxs includes a bunch of solaris -x directives. + options = apxs2_flags.split + options.reject! { |f| f =~ /^\-x/ } + options.reject! { |f| f =~ /^\-Xa/ } + apxs2_flags = options.join(' ') + end + + apxs2_flags.strip! + flags << apxs2_flags + end + if !httpd.nil? && RUBY_PLATFORM =~ /darwin/ + # Add possible universal binary flags. + architectures = [] + `file "#{httpd}"`.split("\n").grep(/for architecture/).each do |line| + line =~ /for architecture (.*?)\)/ + architectures << "-arch #{$1}" + end + flags << architectures.join(' ') + end + flags << portability_cflags + return flags.compact.join(' ').strip + end + memoize :apache2_module_cflags + + # Linker flags that are necessary for linking an Apache module. + # Includes portability_ldflags + def self.apache2_module_ldflags + flags = "-fPIC #{apr_libs} #{apu_libs} #{portability_ldflags}" + flags.strip! + return flags + end + memoize :apache2_module_ldflags + + # The C compiler flags that are necessary for programs that use APR. + def self.apr_flags + return determine_apr_info[0] + end + + # The linker flags that are necessary for linking programs that use APR. + def self.apr_libs + return determine_apr_info[1] + end + + # The C compiler flags that are necessary for programs that use APR-Util. + def self.apu_flags + return determine_apu_info[0] + end + + # The linker flags that are necessary for linking programs that use APR-Util. + def self.apu_libs + return determine_apu_info[1] + end + + + ################ Miscellaneous information ################ + + + # Returns whether it is necessary to use information outputted by + # 'apr-config' and 'apu-config' in order to compile an Apache module. + # When Apache is installed with --with-included-apr, the APR/APU + # headers are placed into the same directory as the Apache headers, + # and so 'apr-config' and 'apu-config' won't be necessary in that case. + def self.apr_config_needed_for_building_apache_modules? + filename = File.join("/tmp/passenger-platform-check-#{Process.pid}.c") + File.open(filename, "w") do |f| + f.puts("#include ") + end + begin + return !system("(gcc #{apache2_module_cflags(false)} -c '#{filename}' -o '#{filename}.o') >/dev/null 2>/dev/null") + ensure + File.unlink(filename) rescue nil + File.unlink("#{filename}.o") rescue nil + end + end + memoize :apr_config_needed_for_building_apache_modules? + + # The current platform's shared library extension ('so' on most Unices). + def self.library_extension + if RUBY_PLATFORM =~ /darwin/ + return "bundle" + else + return "so" + end + end + + # An identifier for the current Linux distribution. nil if the operating system is not Linux. + def self.linux_distro + if RUBY_PLATFORM !~ /linux/ + return nil + end + lsb_release = read_file("/etc/lsb-release") + if lsb_release =~ /Ubuntu/ + return :ubuntu + elsif File.exist?("/etc/debian_version") + return :debian + elsif File.exist?("/etc/redhat-release") + redhat_release = read_file("/etc/redhat-release") + if redhat_release =~ /CentOS/ + return :centos + elsif redhat_release =~ /Fedora/ # is this correct? + return :fedora + else + # On official RHEL distros, the content is in the form of + # "Red Hat Enterprise Linux Server release 5.1 (Tikanga)" + return :rhel + end + elsif File.exist?("/etc/suse-release") + return :suse + elsif File.exist?("/etc/gentoo-release") + return :gentoo + else + return :unknown + end + # TODO: Slackware, Mandrake/Mandriva + end + memoize :linux_distro +end diff --git a/lib/passenger/rack/application_spawner.rb b/lib/phusion_passenger/rack/application_spawner.rb similarity index 67% rename from lib/passenger/rack/application_spawner.rb rename to lib/phusion_passenger/rack/application_spawner.rb index da3cd847..4b04722b 100644 --- a/lib/passenger/rack/application_spawner.rb +++ b/lib/phusion_passenger/rack/application_spawner.rb @@ -14,14 +14,17 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +$LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + "/../../../vendor/rack-0.9.1/lib")) require 'rack' + require 'socket' -require 'passenger/application' -require 'passenger/message_channel' -require 'passenger/abstract_request_handler' -require 'passenger/utils' -require 'passenger/rack/request_handler' -module Passenger +require 'phusion_passenger/application' +require 'phusion_passenger/message_channel' +require 'phusion_passenger/abstract_request_handler' +require 'phusion_passenger/utils' +require 'phusion_passenger/rack/request_handler' + +module PhusionPassenger module Rack # Class for spawning Rack applications. @@ -41,14 +44,18 @@ def self.spawn_application(*args) # - AppInitError: The Rack application raised an exception or called # exit() during startup. # - SystemCallError, IOError, SocketError: Something went wrong. - def spawn_application(app_root, lower_privilege = true, lowest_user = "nobody", environment = "production") + def spawn_application(app_root, options = {}) + options = sanitize_spawn_options(options) + a, b = UNIXSocket.pair - # Double fork in order to prevent zombie processes. - pid = safe_fork(self.class.to_s) do - safe_fork(self.class.to_s) do - a.close - run(MessageChannel.new(b), app_root, lower_privilege, lowest_user, environment) - end + pid = safe_fork(self.class.to_s, true) do + a.close + + file_descriptors_to_leave_open = [0, 1, 2, b.fileno] + NativeSupport.close_all_file_descriptors(file_descriptors_to_leave_open) + close_all_io_objects_for_fds(file_descriptors_to_leave_open) + + run(MessageChannel.new(b), app_root, options) end b.close Process.waitpid(pid) rescue nil @@ -57,25 +64,25 @@ def spawn_application(app_root, lower_privilege = true, lowest_user = "nobody", unmarshal_and_raise_errors(channel, "rack") # No exception was raised, so spawning succeeded. - pid, socket_name, using_abstract_namespace = channel.read + pid, socket_name, socket_type = channel.read if pid.nil? raise IOError, "Connection closed" end owner_pipe = channel.recv_io return Application.new(@app_root, pid, socket_name, - using_abstract_namespace == "true", owner_pipe) + socket_type, owner_pipe) end private - def run(channel, app_root, lower_privilege, lowest_user, environment) + def run(channel, app_root, options) $0 = "Rack: #{app_root}" app = nil success = report_app_init_status(channel) do - ENV['RACK_ENV'] = environment + ENV['RACK_ENV'] = options["environment"] Dir.chdir(app_root) - if lower_privilege - lower_privilege('config.ru', lowest_user) + if options["lower_privilege"] + lower_privilege('config.ru', options["lowest_user"]) end app = load_rack_app end @@ -83,9 +90,9 @@ def run(channel, app_root, lower_privilege, lowest_user, environment) if success reader, writer = IO.pipe begin - handler = RequestHandler.new(reader, app) + handler = RequestHandler.new(reader, app, options) channel.write(Process.pid, handler.socket_name, - handler.using_abstract_namespace?) + handler.socket_type) channel.send_io(writer) writer.close channel.close @@ -105,4 +112,4 @@ def load_rack_app end end # module Rack -end # module Passenger +end # module PhusionPassenger diff --git a/lib/passenger/rack/request_handler.rb b/lib/phusion_passenger/rack/request_handler.rb similarity index 86% rename from lib/passenger/rack/request_handler.rb rename to lib/phusion_passenger/rack/request_handler.rb index c5ebed53..2264f0bd 100644 --- a/lib/passenger/rack/request_handler.rb +++ b/lib/phusion_passenger/rack/request_handler.rb @@ -14,8 +14,8 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -require 'passenger/abstract_request_handler' -module Passenger +require 'phusion_passenger/abstract_request_handler' +module PhusionPassenger module Rack # A request handler for Rack applications. @@ -40,8 +40,8 @@ class RequestHandler < AbstractRequestHandler CRLF = "\r\n" # :nodoc: # +app+ is the Rack application object. - def initialize(owner_pipe, app) - super(owner_pipe) + def initialize(owner_pipe, app, options = {}) + super(owner_pipe, options) @app = app end @@ -66,13 +66,18 @@ def process_request(env, input, output) begin output.write("Status: #{status}#{CRLF}") headers[X_POWERED_BY] = PASSENGER_HEADER - headers.each do |k, vs| - vs.each do |v| - output.write("#{k}: #{v}#{CRLF}") + headers.each_pair do |key, values| + if values.is_a?(String) + values = values.split("\n") + end + values.each do |value| + output.write("#{key}: #{value}#{CRLF}") end end output.write(CRLF) - if body + if body.is_a?(String) + output.write(body) + elsif body body.each do |s| output.write(s) end @@ -84,4 +89,4 @@ def process_request(env, input, output) end end # module Rack -end # module Passenger +end # module PhusionPassenger diff --git a/lib/passenger/railz/application_spawner.rb b/lib/phusion_passenger/railz/application_spawner.rb similarity index 66% rename from lib/passenger/railz/application_spawner.rb rename to lib/phusion_passenger/railz/application_spawner.rb index 49400112..6f2b1ded 100644 --- a/lib/passenger/railz/application_spawner.rb +++ b/lib/phusion_passenger/railz/application_spawner.rb @@ -20,15 +20,17 @@ require 'socket' require 'etc' require 'fcntl' -require 'passenger/application' -require 'passenger/abstract_server' -require 'passenger/application' -require 'passenger/rack/request_handler' -require 'passenger/railz/request_handler' -require 'passenger/exceptions' -require 'passenger/utils' +require 'phusion_passenger/application' +require 'phusion_passenger/abstract_server' +require 'phusion_passenger/application' +require 'phusion_passenger/constants' +require 'phusion_passenger/events' +require 'phusion_passenger/railz/request_handler' +require 'phusion_passenger/rack/request_handler' +require 'phusion_passenger/exceptions' +require 'phusion_passenger/utils' -module Passenger +module PhusionPassenger module Railz # This class is capable of spawning instances of a single Ruby on Rails application. @@ -53,41 +55,45 @@ class Error < AbstractServer::ServerError # 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. + # then an InvalidPath will be raised. # - # If +lower_privilege+ is true, then ApplicationSpawner will attempt to - # switch to the user who owns the application's config/environment.rb, - # and to the default group of that user. + # Additional options are: + # - +lower_privilege+ and +lowest_user+: + # If +lower_privilege+ is true, then ApplicationSpawner will attempt to + # switch to the user who owns the application's config/environment.rb, + # 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. + # 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. # - # The +environment+ argument allows one to specify the RAILS_ENV environment to use. - def initialize(app_root, lower_privilege = true, lowest_user = "nobody", environment = "production") + # - +environment+: + # Allows one to specify the RAILS_ENV environment to use. + # + # All other options will be passed on to RequestHandler. + def initialize(app_root, options = {}) super() begin @app_root = normalize_path(app_root) rescue SystemCallError => e - raise ArgumentError, e.message - rescue ArgumentError + raise InvalidPath, e.message + rescue InvalidPath raise end - @lower_privilege = lower_privilege - @lowest_user = lowest_user - @environment = environment - self.time = Time.now + @options = sanitize_spawn_options(options) + @lower_privilege = @options["lower_privilege"] + @lowest_user = @options["lowest_user"] + @environment = @options["environment"] + self.max_idle_time = DEFAULT_APP_SPAWNER_MAX_IDLE_TIME assert_valid_app_root(@app_root) define_message_handler(:spawn_application, :handle_spawn_application) end @@ -100,15 +106,15 @@ def initialize(app_root, lower_privilege = true, lowest_user = "nobody", environ # - ApplicationSpawner::Error: The ApplicationSpawner server exited unexpectedly. def spawn_application server.write("spawn_application") - pid, socket_name, using_abstract_namespace = server.read + pid, socket_name, socket_type = 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) + socket_type, owner_pipe) rescue SystemCallError, IOError, SocketError => e - raise Error, "The application spawner server exited unexpectedly" + raise Error, "The application spawner server exited unexpectedly: #{e}" end # Spawn an instance of the RoR application. When successful, an Application object @@ -126,30 +132,37 @@ def spawn_application # or called exit() during startup. # - SystemCallError, IOError, SocketError: Something went wrong. def spawn_application! - # Double fork to prevent zombie processes. a, b = UNIXSocket.pair - pid = safe_fork(self.class.to_s) do - safe_fork('application') do - begin - a.close - channel = MessageChannel.new(b) - success = report_app_init_status(channel) do - ENV['RAILS_ENV'] = @environment - Dir.chdir(@app_root) - if @lower_privilege - lower_privilege('config/environment.rb', @lowest_user) - end - require 'config/environment' - require 'dispatcher' - end - if success - start_request_handler(channel) - end - rescue SignalException => e - if e.message != AbstractRequestHandler::HARD_TERMINATION_SIGNAL && - e.message != AbstractRequestHandler::SOFT_TERMINATION_SIGNAL - raise + pid = safe_fork('application', true) do + begin + a.close + + file_descriptors_to_leave_open = [0, 1, 2, b.fileno] + NativeSupport.close_all_file_descriptors(file_descriptors_to_leave_open) + close_all_io_objects_for_fds(file_descriptors_to_leave_open) + + channel = MessageChannel.new(b) + success = report_app_init_status(channel) do + ENV['RAILS_ENV'] = @environment + Dir.chdir(@app_root) + if @lower_privilege + lower_privilege('config/environment.rb', @lowest_user) end + + # require Rails' environment, using the same path as the original rails dispatcher, + # which normally does: require File.dirname(__FILE__) + "/../config/environment" + # thus avoiding the possibility of including the same file twice. + require 'public/../config/environment' + + require 'dispatcher' + end + if success + start_request_handler(channel, false) + end + rescue SignalException => e + if e.message != AbstractRequestHandler::HARD_TERMINATION_SIGNAL && + e.message != AbstractRequestHandler::SOFT_TERMINATION_SIGNAL + raise end end end @@ -160,13 +173,13 @@ def spawn_application! unmarshal_and_raise_errors(channel) # No exception was raised, so spawning succeeded. - pid, socket_name, using_abstract_namespace = channel.read + pid, socket_name, socket_type = channel.read if pid.nil? raise IOError, "Connection closed" end owner_pipe = channel.recv_io return Application.new(@app_root, pid, socket_name, - using_abstract_namespace == "true", owner_pipe) + socket_type, owner_pipe) end # Overrided from AbstractServer#start. @@ -179,9 +192,9 @@ def start super begin unmarshal_and_raise_errors(server) - rescue IOError, SystemCallError, SocketError + rescue IOError, SystemCallError, SocketError => e stop - raise Error, "The application spawner server exited unexpectedly" + raise Error, "The application spawner server exited unexpectedly: #{e}" rescue stop raise @@ -267,7 +280,12 @@ def load_environment_with_passenger else require_dependency 'application' end - if GC.copy_on_write_friendly? + + # - No point in preloading the application sources if the garbage collector + # isn't copy-on-write friendly. + # - Rails >= 2.2 already preloads application sources by default, so no need + # to do that again. + if GC.copy_on_write_friendly? && !::Rails::Initializer.respond_to?(:load_application_classes) Dir.glob('app/{models,controllers,helpers}/*.rb').each do |file| require_dependency normalize_path(file) end @@ -275,25 +293,24 @@ def load_environment_with_passenger end def handle_spawn_application - # Double fork to prevent zombie processes. - pid = safe_fork(self.class.to_s) do - safe_fork('application') do - begin - start_request_handler(client) - rescue SignalException => e - if e.message != AbstractRequestHandler::HARD_TERMINATION_SIGNAL && - e.message != AbstractRequestHandler::SOFT_TERMINATION_SIGNAL - raise - end + safe_fork('application', true) do + begin + start_request_handler(client, true) + rescue SignalException => e + if e.message != AbstractRequestHandler::HARD_TERMINATION_SIGNAL && + e.message != AbstractRequestHandler::SOFT_TERMINATION_SIGNAL + raise end end end - Process.waitpid(pid) end # Initialize the request handler and enter its main loop. # Spawn information will be sent back via _channel_. - def start_request_handler(channel) + # The _forked_ argument indicates whether a new process was forked off + # after loading environment.rb (i.e. whether smart spawning is being + # used). + def start_request_handler(channel, forked) $0 = "Rails: #{@app_root}" reader, writer = IO.pipe begin @@ -308,24 +325,27 @@ def start_request_handler(channel) if Rails::VERSION::STRING >= '2.3.0' rack_app = ::ActionController::Dispatcher.new - handler = Rack::RequestHandler.new(reader, rack_app) + handler = Rack::RequestHandler.new(reader, rack_app, @options) else - handler = RequestHandler.new(reader) + handler = RequestHandler.new(reader, @options) end channel.write(Process.pid, handler.socket_name, - handler.using_abstract_namespace?) + handler.socket_type) channel.send_io(writer) writer.close channel.close + + PhusionPassenger.call_event(:starting_worker_process, forked) handler.main_loop ensure channel.close rescue nil writer.close rescue nil handler.cleanup rescue nil + PhusionPassenger.call_event(:stopping_worker_process) end end end end # module Railz -end # module Passenger +end # module PhusionPassenger diff --git a/lib/passenger/railz/cgi_fixed.rb b/lib/phusion_passenger/railz/cgi_fixed.rb similarity index 97% rename from lib/passenger/railz/cgi_fixed.rb rename to lib/phusion_passenger/railz/cgi_fixed.rb index 44533cf9..9d385513 100644 --- a/lib/passenger/railz/cgi_fixed.rb +++ b/lib/phusion_passenger/railz/cgi_fixed.rb @@ -26,7 +26,7 @@ require 'cgi' -module Passenger +module PhusionPassenger module Railz # Modifies CGI so that we can use it. Main thing it does is expose @@ -65,4 +65,4 @@ def stdoutput end end # module Railz -end # module Passenger +end # module PhusionPassenger diff --git a/lib/passenger/railz/framework_spawner.rb b/lib/phusion_passenger/railz/framework_spawner.rb similarity index 71% rename from lib/passenger/railz/framework_spawner.rb rename to lib/phusion_passenger/railz/framework_spawner.rb index 902a75ed..11b2272c 100644 --- a/lib/passenger/railz/framework_spawner.rb +++ b/lib/phusion_passenger/railz/framework_spawner.rb @@ -17,12 +17,13 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. require 'rubygems' -require 'passenger/abstract_server' -require 'passenger/railz/application_spawner' -require 'passenger/exceptions' -require 'passenger/constants' -require 'passenger/utils' -module Passenger +require 'phusion_passenger/abstract_server' +require 'phusion_passenger/abstract_server_collection' +require 'phusion_passenger/railz/application_spawner' +require 'phusion_passenger/exceptions' +require 'phusion_passenger/constants' +require 'phusion_passenger/utils' +module PhusionPassenger module Railz # This class is capable of spawning Ruby on Rails application instances @@ -45,12 +46,9 @@ class FrameworkSpawner < AbstractServer class Error < AbstractServer::ServerError end - # An attribute, used internally. This should not be used outside Passenger. - attr_accessor :time - # Creates a new instance of FrameworkSpawner. # - # Valid options: + # Valid options are: # - :version: The Ruby on Rails version to use. It is not checked whether # this version is actually installed. # - :vendor: The directory to the vendor Rails framework to use. This is @@ -58,6 +56,8 @@ class Error < AbstractServer::ServerError # # It is not allowed to specify both +version+ and +vendor+. # + # All other options will be passed on to ApplicationSpawner and RequestHandler. + # # Note that the specified Rails framework will be loaded during the entire life time # of the FrameworkSpawner server. If you wish to reload the Rails framework's code, # then restart the server by calling AbstractServer#stop and AbstractServer#start. @@ -66,7 +66,7 @@ def initialize(options = {}) raise ArgumentError, "The 'options' argument not seem to be an options hash" end @version = options[:version] - @vendor = options[:vendor] + @vendor = options[:vendor] if !@version && !@vendor raise ArgumentError, "Either the 'version' or the 'vendor' option must specified" elsif @version && @vendor @@ -74,6 +74,7 @@ def initialize(options = {}) end super() + self.max_idle_time = DEFAULT_FRAMEWORK_SPAWNER_MAX_IDLE_TIME define_message_handler(:spawn_application, :handle_spawn_application) define_message_handler(:reload, :handle_reload) end @@ -86,7 +87,12 @@ def initialize(options = {}) def start super begin - status = server.read[0] + result = server.read + if result.nil? + raise Error, "The framework spawner server exited unexpectedly." + else + status = result[0] + end if status == 'exception' child_exception = unmarshal_exception(server.read_scalar) stop @@ -111,8 +117,23 @@ def start # When successful, an Application object will be returned, which represents # the spawned RoR application. # - # See ApplicationSpawner.new for an explanation of the +lower_privilege+, - # +lowest_user+ and +environment+ parameters. + # The following options are allowed: + # - +lower_privilege+ and +lowest_user+: + # If +lower_privilege+ is true, then ApplicationSpawner will attempt to + # switch to the user who owns the application's config/environment.rb, + # 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. + # + # - +environment+: + # Allows one to specify the RAILS_ENV environment to use. + # + # All other options will be passed on to ApplicationSpawner and RequestHandler. # # FrameworkSpawner will internally cache the code of applications, in order to # speed up future spawning attempts. This implies that, if you've changed @@ -122,30 +143,37 @@ def start # # Raises: # - AbstractServer::ServerNotStarted: The FrameworkSpawner server hasn't already been started. - # - ArgumentError: +app_root+ doesn't appear to be a valid Ruby on Rails application root. + # - InvalidAppRoot: +app_root+ doesn't appear to be a valid Ruby on Rails application root. # - AppInitError: The application raised an exception or called exit() during startup. # - ApplicationSpawner::Error: The ApplicationSpawner server exited unexpectedly. # - FrameworkSpawner::Error: The FrameworkSpawner server exited unexpectedly. - def spawn_application(app_root, lower_privilege = true, lowest_user = "nobody", environment = "production") + def spawn_application(app_root, options = {}) app_root = normalize_path(app_root) assert_valid_app_root(app_root) + options = sanitize_spawn_options(options) + options["app_root"] = app_root + exception_to_propagate = nil begin - server.write("spawn_application", app_root, lower_privilege, lowest_user, environment) + server.write("spawn_application", *options.to_a.flatten) result = server.read if result.nil? raise IOError, "Connection closed" end if result[0] == 'exception' - raise unmarshal_exception(server.read_scalar) + e = unmarshal_exception(server.read_scalar) + if e.respond_to?(:child_exception) && e.child_exception + #print_exception(self.class.to_s, e.child_exception) + end + raise e else - pid, listen_socket_name, using_abstract_namespace = server.read + pid, listen_socket_name, socket_type = server.read if pid.nil? raise IOError, "Connection closed" end owner_pipe = server.recv_io return Application.new(app_root, pid, listen_socket_name, - using_abstract_namespace == "true", owner_pipe) + socket_type, owner_pipe) end rescue SystemCallError, IOError, SocketError => e raise Error, "The framework spawner server exited unexpectedly" @@ -191,16 +219,7 @@ def before_fork # :nodoc: # Overrided method. def initialize_server # :nodoc: $0 = "Passenger FrameworkSpawner: #{@version || @vendor}" - @spawners = {} - @spawners_lock = Mutex.new - @spawners_cond = ConditionVariable.new - @spawners_cleaner = Thread.new do - begin - spawners_cleaner_main_loop - rescue Exception => e - print_exception(self.class.to_s, e) - end - end + @spawners = AbstractServerCollection.new begin preload_rails rescue StandardError, ScriptError, NoMemoryError => e @@ -213,13 +232,7 @@ def initialize_server # :nodoc: # Overrided method. def finalize_server # :nodoc: - @spawners_lock.synchronize do - @spawners_cond.signal - end - @spawners_cleaner.join - @spawners.each_value do |spawner| - spawner.stop - end + @spawners.cleanup end private @@ -258,31 +271,36 @@ def preload_rails Object.send(:remove_const, :RAILS_ROOT) end - def handle_spawn_application(app_root, lower_privilege, lowest_user, environment) - lower_privilege = lower_privilege == "true" - @spawners_lock.synchronize do - spawner = @spawners[app_root] - if spawner.nil? - begin - spawner = ApplicationSpawner.new(app_root, - lower_privilege, lowest_user, - environment) - spawner.start - rescue ArgumentError, AppInitError, ApplicationSpawner::Error => e - client.write('exception') - client.write_scalar(marshal_exception(e)) - if e.child_exception.is_a?(LoadError) - # A source file failed to load, maybe because of a - # missing gem. If that's the case then the sysadmin - # will install probably the gem. So we clear RubyGems's - # cache so that it can detect new gems. - Gem.clear_paths + def handle_spawn_application(*options) + options = Hash[*options] + options["lower_privilege"] = options["lower_privilege"] == "true" + options["app_spawner_timeout"] = options["app_spawner_timeout"].to_i + options["memory_limit"] = options["memory_limit"].to_i + + app = nil + app_root = options["app_root"] + @spawners.synchronize do + begin + spawner = @spawners.lookup_or_add(app_root) do + spawner = ApplicationSpawner.new(app_root, options) + if options["app_spawner_timeout"] && options["app_spawner_timeout"] != -1 + spawner.max_idle_time = options["app_spawner_timeout"] end - return + spawner.start + spawner end - @spawners[app_root] = spawner + rescue ArgumentError, AppInitError, ApplicationSpawner::Error => e + client.write('exception') + client.write_scalar(marshal_exception(e)) + if e.child_exception.is_a?(LoadError) + # A source file failed to load, maybe because of a + # missing gem. If that's the case then the sysadmin + # will install probably the gem. So we clear RubyGems's + # cache so that it can detect new gems. + Gem.clear_paths + end + return end - spawner.time = Time.now begin app = spawner.spawn_application rescue ApplicationSpawner::Error => e @@ -292,53 +310,23 @@ def handle_spawn_application(app_root, lower_privilege, lowest_user, environment client.write_scalar(marshal_exception(e)) return end - client.write('success') - client.write(app.pid, app.listen_socket_name, app.using_abstract_namespace?) - client.send_io(app.owner_pipe) - app.close end + client.write('success') + client.write(app.pid, app.listen_socket_name, app.listen_socket_type) + client.send_io(app.owner_pipe) + app.close end def handle_reload(app_root = nil) - @spawners_lock.synchronize do - if app_root.nil? - @spawners.each_value do |spawner| - spawner.stop - end - @spawners.clear + @spawners.synchronize do + if app_root + @spawners.delete(app_root) else - spawner = @spawners[app_root] - if spawner - spawner.stop - @spawners.delete(app_root) - end - end - end - end - - # The main loop for the spawners cleaner thread. - # This thread checks the spawners list every APP_SPAWNER_CLEAN_INTERVAL seconds, - # and stops application spawners that have been idle for more than - # APP_SPAWNER_MAX_IDLE_TIME seconds. - def spawners_cleaner_main_loop - @spawners_lock.synchronize do - while true - if @spawners_cond.timed_wait(@spawners_lock, APP_SPAWNER_CLEAN_INTERVAL) - break - else - current_time = Time.now - @spawners.keys.each do |key| - spawner = @spawners[key] - if current_time - spawner.time > APP_SPAWNER_MAX_IDLE_TIME - spawner.stop - @spawners.delete(key) - end - end - end + @spawners.clear end end end end end # module Railz -end # module Passenger +end # module PhusionPassenger diff --git a/lib/passenger/railz/request_handler.rb b/lib/phusion_passenger/railz/request_handler.rb similarity index 88% rename from lib/passenger/railz/request_handler.rb rename to lib/phusion_passenger/railz/request_handler.rb index 44d1245d..caf95627 100644 --- a/lib/passenger/railz/request_handler.rb +++ b/lib/phusion_passenger/railz/request_handler.rb @@ -14,9 +14,9 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -require 'passenger/abstract_request_handler' -require 'passenger/railz/cgi_fixed' -module Passenger +require 'phusion_passenger/abstract_request_handler' +require 'phusion_passenger/railz/cgi_fixed' +module PhusionPassenger module Railz # A request handler for Ruby on Rails applications. @@ -24,8 +24,8 @@ class RequestHandler < AbstractRequestHandler NINJA_PATCHING_LOCK = Mutex.new @@ninja_patched_action_controller = false - def initialize(owner_pipe) - super(owner_pipe) + def initialize(owner_pipe, options = {}) + super(owner_pipe, options) NINJA_PATCHING_LOCK.synchronize do ninja_patch_action_controller end @@ -58,4 +58,4 @@ def perform_action(*whatever) end end # module Railz -end # module Passenger +end # module PhusionPassenger diff --git a/lib/passenger/simple_benchmarking.rb b/lib/phusion_passenger/simple_benchmarking.rb similarity index 100% rename from lib/passenger/simple_benchmarking.rb rename to lib/phusion_passenger/simple_benchmarking.rb diff --git a/lib/passenger/spawn_manager.rb b/lib/phusion_passenger/spawn_manager.rb similarity index 56% rename from lib/passenger/spawn_manager.rb rename to lib/phusion_passenger/spawn_manager.rb index d450096b..3b3db661 100644 --- a/lib/passenger/spawn_manager.rb +++ b/lib/phusion_passenger/spawn_manager.rb @@ -16,10 +16,17 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -require 'passenger/abstract_server' -require 'passenger/constants' -require 'passenger/utils' -module Passenger +require 'phusion_passenger/abstract_server' +require 'phusion_passenger/abstract_server_collection' +require 'phusion_passenger/constants' +require 'phusion_passenger/utils' + +# Define a constant with a name that's unlikely to clash with anything the +# application defines, so that they can detect whether they're running under +# Phusion Passenger. +IN_PHUSION_PASSENGER = true + +module PhusionPassenger # The spawn manager is capable of spawning Ruby on Rails or Rack application # instances. It acts like a simple fascade for the rest of the spawn manager @@ -30,7 +37,7 @@ module Passenger # tested. Don't forget to call cleanup after the server's main loop has # finished. # -# == Ruby on Rails optimizations === +# == Ruby on Rails optimizations # # Spawning a Ruby on Rails application is usually slow. But SpawnManager # will preload and cache Ruby on Rails frameworks, as well as application @@ -47,27 +54,25 @@ class SpawnManager < AbstractServer def initialize super() - @spawners = {} - @lock = Mutex.new - @cond = ConditionVariable.new - @cleaner_thread = Thread.new do - cleaner_thread_main - end + @spawners = AbstractServerCollection.new define_message_handler(:spawn_application, :handle_spawn_application) define_message_handler(:reload, :handle_reload) define_signal_handler('SIGHUP', :reload) + # Start garbage collector in order to free up some existing + # heap slots. This prevents the heap from growing unnecessarily + # during the startup phase. GC.start if GC.copy_on_write_friendly? # Preload libraries for copy-on-write semantics. require 'base64' - require 'passenger/application' - require 'passenger/railz/framework_spawner' - require 'passenger/railz/application_spawner' - require 'passenger/rack/application_spawner' - require 'passenger/html_template' - require 'passenger/platform_info' - require 'passenger/exceptions' + require 'phusion_passenger/application' + require 'phusion_passenger/railz/framework_spawner' + require 'phusion_passenger/railz/application_spawner' + require 'phusion_passenger/rack/application_spawner' + require 'phusion_passenger/html_template' + require 'phusion_passenger/platform_info' + require 'phusion_passenger/exceptions' # Commonly used libraries. ['mysql', 'sqlite3'].each do |lib| @@ -80,51 +85,84 @@ def initialize end end - # Spawn a RoR application When successful, an Application object will be - # returned, which represents the spawned RoR application. - # - # See Railz::ApplicationSpawner.new for an explanation of the +lower_privilege+, - # +lowest_user+ and +environment+ parameters. + # Spawn an application with the given spawn options. When successful, an + # Application object will be returned, which represents the spawned application. + # At least one option must be given: +app_root+. This is the application's root + # folder. # - # The +spawn_method+ argument may be one of "smart" or "conservative". - # When "smart" is specified (the default), SpawnManager will internally cache the - # code of applications, in order to speed up future spawning attempts. This implies - # that, if you've changed the application's code, you must do one of these things: - # - Restart this SpawnManager by calling AbstractServer#stop, then AbstractServer#start. - # - Reload the application by calling reload with the correct app_root argument. - # Caching however can be incompatible with some applications. + # Other options are: # - # The "conservative" spawning method does not involve any caching at all. - # Spawning will be slower, but is guaranteed to be compatible with all applications. + # ['lower_privilege', 'lowest_user' and 'environment'] + # See Railz::ApplicationSpawner.new for an explanation of these options. + # + # ['app_type'] + # What kind of application is being spawned. Either "rails" (default), "rack" or "wsgi". + # + # ['spawn_method'] + # May be one of "smart", "smart-lv2" or "conservative". When "smart" is specified, + # SpawnManager will internally cache the code of Rails applications, in + # order to speed up future spawning attempts. This implies that, if you've changed + # the application's + # code, you must do one of these things: + # - Restart this SpawnManager by calling AbstractServer#stop, then AbstractServer#start. + # - Reload the application by calling reload with the correct app_root argument. + # + # "smart" caches the Rails framework code in a framework spawner server, and application + # code in an application spawner server. Sometimes it is desirable to skip the + # framework spawning and going directly for the application spawner instead. The + # "smart-lv2" method allows you to do that. + # + # Caching however can be incompatible with some applications. The "conservative" + # spawning method does not involve any caching at all. Spawning will be slower, + # but is guaranteed to be compatible with all applications. + # + # The default spawn method is "smart-lv2". + # + # ['framework_spawner_timeout' and 'app_spawner_timeout'] + # These options allow you to specify the maximum idle timeout, in seconds, of the + # framework spawner servers and application spawner servers that will be started under + # the hood. These options are only used if +app_type+ equals "rails". + # + # A timeout of 0 means that the spawner server should never idle timeout. A timeout of + # -1 means that the default timeout value should be used. The default value is -1. # - # Raises: - # - ArgumentError: +app_root+ doesn't appear to be a valid Ruby on Rails application root. + # Exceptions: + # - InvalidPath: +app_root+ doesn't appear to be a valid Ruby on Rails application root. # - VersionNotFound: The Ruby on Rails framework version that the given application requires # is not installed. # - AbstractServer::ServerError: One of the server processes exited unexpectedly. # - FrameworkInitError: The Ruby on Rails framework that the application requires could not be loaded. # - AppInitError: The application raised an exception or called exit() during startup. - def spawn_application(app_root, lower_privilege = true, lowest_user = "nobody", - environment = "production", spawn_method = "smart", - app_type = "rails") - if app_type == "rack" + def spawn_application(options) + if !options["app_root"] + raise ArgumentError, "The 'app_root' option must be given." + end + options = sanitize_spawn_options(options) + + if options["app_type"] == "rails" + if !defined?(Railz::FrameworkSpawner) + require 'phusion_passenger/application' + require 'phusion_passenger/railz/framework_spawner' + require 'phusion_passenger/railz/application_spawner' + end + return spawn_rails_application(options) + elsif options["app_type"] == "rack" if !defined?(Rack::ApplicationSpawner) - require 'passenger/rack/application_spawner' + require 'phusion_passenger/rack/application_spawner' end - return Rack::ApplicationSpawner.spawn_application(app_root, - lower_privilege, lowest_user, environment) - elsif app_type == "wsgi" - require 'passenger/wsgi/application_spawner' - return WSGI::ApplicationSpawner.spawn_application(app_root, - lower_privilege, lowest_user, environment) + return Rack::ApplicationSpawner.spawn_application( + options["app_root"], options + ) + elsif options["app_type"] == "wsgi" + require 'phusion_passenger/wsgi/application_spawner' + return WSGI::ApplicationSpawner.spawn_application( + options["app_root"], + options["lower_privilege"], + options["lowest_user"], + options["environment"] + ) else - if !defined?(Railz::FrameworkSpawner) - require 'passenger/application' - require 'passenger/railz/framework_spawner' - require 'passenger/railz/application_spawner' - end - return spawn_rails_application(app_root, lower_privilege, lowest_user, - environment, spawn_method) + raise ArgumentError, "Unknown 'app_type' value '#{options["app_type"]}'." end end @@ -145,23 +183,27 @@ def reload(app_root = nil) if app_root begin app_root = normalize_path(app_root) - rescue ArgumentError + rescue InvalidPath end end - @lock.synchronize do + @spawners.synchronize do if app_root # Delete associated ApplicationSpawner. - key = "app:#{app_root}" - spawner = @spawners[key] - if spawner - if spawner.started? - spawner.stop + @spawners.delete("app:#{app_root}") + else + # Delete all ApplicationSpawners. + keys_to_delete = [] + @spawners.each_pair do |key, spawner| + if spawner.is_a?(Railz::ApplicationSpawner) + keys_to_delete << key end + end + keys_to_delete.each do |key| @spawners.delete(key) end end - @spawners.each_value do |spawner| - # Reload FrameworkSpawners. + @spawners.each do |spawner| + # Reload all FrameworkSpawners. if spawner.respond_to?(:reload) spawner.reload(app_root) end @@ -171,86 +213,77 @@ def reload(app_root = nil) # Cleanup resources. Should be called when this SpawnManager is no longer needed. def cleanup - @lock.synchronize do - @cond.signal - end - @cleaner_thread.join - @lock.synchronize do - @spawners.each_value do |spawner| - if spawner.started? - spawner.stop - end - end - @spawners.clear - end + @spawners.cleanup end private - def spawn_rails_application(app_root, lower_privilege, lowest_user, - environment, spawn_method) - if spawn_method == "smart" + def spawn_rails_application(options) + spawn_method = options["spawn_method"] + app_root = options["app_root"] + + if [nil, "", "smart", "smart-lv2"].include?(spawn_method) spawner_must_be_started = true - framework_version = Application.detect_framework_version(app_root) + if spawn_method != "smart-lv2" + framework_version = Application.detect_framework_version(app_root) + end if framework_version.nil? || framework_version == :vendor app_root = normalize_path(app_root) key = "app:#{app_root}" create_spawner = proc do - Railz::ApplicationSpawner.new(app_root, lower_privilege, - lowest_user, environment) + Railz::ApplicationSpawner.new(app_root, options) end + spawner_timeout = options["app_spawner_timeout"] else key = "version:#{framework_version}" create_spawner = proc do Railz::FrameworkSpawner.new(:version => framework_version) end + spawner_timeout = options["framework_spawner_timeout"] end else app_root = normalize_path(app_root) key = "app:#{app_root}" create_spawner = proc do - Railz::ApplicationSpawner.new(app_root, lower_privilege, lowest_user, environment) + Railz::ApplicationSpawner.new(app_root, options) end + spawner_timeout = options["app_spawner_timeout"] spawner_must_be_started = false end - spawner = nil - @lock.synchronize do - spawner = @spawners[key] - if !spawner + @spawners.synchronize do + spawner = @spawners.lookup_or_add(key) do spawner = create_spawner.call + if spawner_timeout != -1 + spawner.max_idle_time = spawner_timeout + end if spawner_must_be_started spawner.start end - @spawners[key] = spawner + spawner end - spawner.time = Time.now begin if spawner.is_a?(Railz::FrameworkSpawner) - return spawner.spawn_application(app_root, lower_privilege, - lowest_user, environment) + return spawner.spawn_application(app_root, options) elsif spawner.started? return spawner.spawn_application else return spawner.spawn_application! end rescue AbstractServer::ServerError - if spawner.started? - spawner.stop - end @spawners.delete(key) raise end end end - def handle_spawn_application(app_root, lower_privilege, lowest_user, environment, - spawn_method, app_type) - lower_privilege = lower_privilege == "true" + def handle_spawn_application(*options) + options = sanitize_spawn_options(Hash[*options]) app = nil + app_root = options["app_root"] + app_type = options["app_type"] begin - app = spawn_application(app_root, lower_privilege, lowest_user, - environment, spawn_method, app_type) - rescue ArgumentError => e + app = spawn_application(options) + rescue InvalidPath => e send_error_page(client, 'invalid_app_root', :error => e, :app_root => app_root) rescue AbstractServer::ServerError => e send_error_page(client, 'general_error', :error => e) @@ -269,7 +302,7 @@ def handle_spawn_application(app_root, lower_privilege, lowest_user, environment Gem.clear_paths send_error_page(client, 'load_error', :error => e, :app_root => app_root, :app_name => app_name(app_type)) - elsif e.child_exception.nil? + elsif e.child_exception.is_a?(SystemExit) send_error_page(client, 'app_exited_during_initialization', :error => e, :app_root => app_root, :app_name => app_name(app_type)) else @@ -282,7 +315,8 @@ def handle_spawn_application(app_root, lower_privilege, lowest_user, environment if app begin client.write('ok') - client.write(app.pid, app.listen_socket_name, app.using_abstract_namespace?) + client.write(app.pid, app.listen_socket_name, + app.listen_socket_type) client.send_io(app.owner_pipe) rescue Errno::EPIPE # The Apache module may be interrupted during a spawn command, @@ -297,35 +331,9 @@ def handle_reload(app_root) reload(app_root) end - def cleaner_thread_main - @lock.synchronize do - while true - if @cond.timed_wait(@lock, SPAWNER_CLEAN_INTERVAL) - break - else - current_time = Time.now - @spawners.keys.each do |key| - spawner = @spawners[key] - if spawner.is_a?(Railz::FrameworkSpawner) - max_idle_time = FRAMEWORK_SPAWNER_MAX_IDLE_TIME - else - max_idle_time = APP_SPAWNER_MAX_IDLE_TIME - end - if current_time - spawner.time > max_idle_time - if spawner.started? - spawner.stop - end - @spawners.delete(key) - end - end - end - end - end - end - def send_error_page(channel, template_name, options = {}) - require 'passenger/html_template' unless defined?(HTMLTemplate) - require 'passenger/platform_info' unless defined?(PlatformInfo) + require 'phusion_passenger/html_template' unless defined?(HTMLTemplate) + require 'phusion_passenger/platform_info' unless defined?(PlatformInfo) options["enterprisey"] = File.exist?("#{File.dirname(__FILE__)}/../../enterprisey.txt") || File.exist?("/etc/passenger_enterprisey.txt") data = HTMLTemplate.new(template_name, options).result @@ -359,4 +367,4 @@ def app_name(app_type) end end -end # module Passenger +end # module PhusionPassenger diff --git a/lib/passenger/templates/apache2_config_snippets.txt.erb b/lib/phusion_passenger/templates/apache2_config_snippets.txt.erb similarity index 100% rename from lib/passenger/templates/apache2_config_snippets.txt.erb rename to lib/phusion_passenger/templates/apache2_config_snippets.txt.erb diff --git a/lib/passenger/templates/apache_must_be_compiled_with_compatible_mpm.txt.erb b/lib/phusion_passenger/templates/apache_must_be_compiled_with_compatible_mpm.txt.erb similarity index 100% rename from lib/passenger/templates/apache_must_be_compiled_with_compatible_mpm.txt.erb rename to lib/phusion_passenger/templates/apache_must_be_compiled_with_compatible_mpm.txt.erb diff --git a/lib/phusion_passenger/templates/app_exited_during_initialization.html.erb b/lib/phusion_passenger/templates/app_exited_during_initialization.html.erb new file mode 100644 index 00000000..40e98b4a --- /dev/null +++ b/lib/phusion_passenger/templates/app_exited_during_initialization.html.erb @@ -0,0 +1,38 @@ +<% layout 'error_layout', :title => "#{@app_name} application could not be started" do %> +

<%= @app_name %> application could not be started

+
+ + The application has exited during startup (i.e. during the evaluation of + config/environment.rb). + <% if @error.stderr %> + The error message can be found below. To solve this problem, please + follow any instructions in the error message. + <% else %> + The error message may have been written to the web server's log file. + Please check the web server's log file (i.e. not the + (Rails) application's log file) to find out why the application + exited. + +

If that doesn't help, then please use the backtrace below to debug + the problem.

+ <% end %> + +
+ <% if @error.stderr %> +
Error message:
+
<%=h @error.stderr %>
+ <% end %> + +
Application root:
+
+ <%=h @app_root %> +
+ +
Backtrace:
+
+ <%= backtrace_html_for(@error.child_exception) %> +
+
+ +
+<% end %> diff --git a/lib/passenger/templates/app_init_error.html.erb b/lib/phusion_passenger/templates/app_init_error.html.erb similarity index 100% rename from lib/passenger/templates/app_init_error.html.erb rename to lib/phusion_passenger/templates/app_init_error.html.erb diff --git a/lib/passenger/templates/database_error.html.erb b/lib/phusion_passenger/templates/database_error.html.erb similarity index 100% rename from lib/passenger/templates/database_error.html.erb rename to lib/phusion_passenger/templates/database_error.html.erb diff --git a/lib/passenger/templates/deployment_example.txt.erb b/lib/phusion_passenger/templates/deployment_example.txt.erb similarity index 89% rename from lib/passenger/templates/deployment_example.txt.erb rename to lib/phusion_passenger/templates/deployment_example.txt.erb index e376fb22..5944a349 100644 --- a/lib/passenger/templates/deployment_example.txt.erb +++ b/lib/phusion_passenger/templates/deployment_example.txt.erb @@ -6,7 +6,7 @@ to your Apache configuration file, and set its DocumentRoot to ServerName www.yourhost.com - DocumentRoot /somewhere/public + DocumentRoot /somewhere/public # <-- be sure to point to 'public'! And that's it! You may also want to check the Users Guide for security and diff --git a/lib/passenger/templates/error_layout.css b/lib/phusion_passenger/templates/error_layout.css similarity index 100% rename from lib/passenger/templates/error_layout.css rename to lib/phusion_passenger/templates/error_layout.css diff --git a/lib/passenger/templates/error_layout.html.erb b/lib/phusion_passenger/templates/error_layout.html.erb similarity index 100% rename from lib/passenger/templates/error_layout.html.erb rename to lib/phusion_passenger/templates/error_layout.html.erb diff --git a/lib/passenger/templates/framework_init_error.html.erb b/lib/phusion_passenger/templates/framework_init_error.html.erb similarity index 100% rename from lib/passenger/templates/framework_init_error.html.erb rename to lib/phusion_passenger/templates/framework_init_error.html.erb diff --git a/lib/passenger/templates/general_error.html.erb b/lib/phusion_passenger/templates/general_error.html.erb similarity index 100% rename from lib/passenger/templates/general_error.html.erb rename to lib/phusion_passenger/templates/general_error.html.erb diff --git a/lib/passenger/templates/invalid_app_root.html.erb b/lib/phusion_passenger/templates/invalid_app_root.html.erb similarity index 80% rename from lib/passenger/templates/invalid_app_root.html.erb rename to lib/phusion_passenger/templates/invalid_app_root.html.erb index 5a6feb00..750d207c 100644 --- a/lib/passenger/templates/invalid_app_root.html.erb +++ b/lib/phusion_passenger/templates/invalid_app_root.html.erb @@ -2,7 +2,7 @@

Cannot start Ruby on Rails application

- The directory <%=h @app_root %> + The directory "<%=h @app_root %>" does not appear to be a valid Ruby on Rails application root.
diff --git a/lib/passenger/templates/load_error.html.erb b/lib/phusion_passenger/templates/load_error.html.erb similarity index 100% rename from lib/passenger/templates/load_error.html.erb rename to lib/phusion_passenger/templates/load_error.html.erb diff --git a/lib/passenger/templates/no_write_permission_to_passenger_root.txt.erb b/lib/phusion_passenger/templates/no_write_permission_to_passenger_root.txt.erb similarity index 100% rename from lib/passenger/templates/no_write_permission_to_passenger_root.txt.erb rename to lib/phusion_passenger/templates/no_write_permission_to_passenger_root.txt.erb diff --git a/lib/passenger/templates/possible_solutions_for_compilation_and_installation_problems.txt.erb b/lib/phusion_passenger/templates/possible_solutions_for_compilation_and_installation_problems.txt.erb similarity index 100% rename from lib/passenger/templates/possible_solutions_for_compilation_and_installation_problems.txt.erb rename to lib/phusion_passenger/templates/possible_solutions_for_compilation_and_installation_problems.txt.erb diff --git a/lib/passenger/templates/run_installer_as_root.txt.erb b/lib/phusion_passenger/templates/run_installer_as_root.txt.erb similarity index 100% rename from lib/passenger/templates/run_installer_as_root.txt.erb rename to lib/phusion_passenger/templates/run_installer_as_root.txt.erb diff --git a/lib/passenger/templates/version_not_found.html.erb b/lib/phusion_passenger/templates/version_not_found.html.erb similarity index 100% rename from lib/passenger/templates/version_not_found.html.erb rename to lib/phusion_passenger/templates/version_not_found.html.erb diff --git a/lib/passenger/templates/welcome.txt.erb b/lib/phusion_passenger/templates/welcome.txt.erb similarity index 100% rename from lib/passenger/templates/welcome.txt.erb rename to lib/phusion_passenger/templates/welcome.txt.erb diff --git a/lib/passenger/utils.rb b/lib/phusion_passenger/utils.rb similarity index 57% rename from lib/passenger/utils.rb rename to lib/phusion_passenger/utils.rb index 476e05fe..0c02a400 100644 --- a/lib/passenger/utils.rb +++ b/lib/phusion_passenger/utils.rb @@ -18,17 +18,19 @@ require 'rubygems' require 'thread' -if RUBY_PLATFORM != "java" && (RUBY_VERSION < "1.8.6" || (RUBY_VERSION == "1.8.6" && RUBY_PATCHLEVEL < 110)) +if (!defined?(RUBY_ENGINE) || RUBY_ENGINE == "ruby") && RUBY_VERSION < "1.8.7" require 'fastthread' end require 'pathname' require 'etc' -require 'passenger/exceptions' -if RUBY_PLATFORM != "java" - require 'passenger/native_support' +require 'fcntl' +require 'tempfile' +require 'phusion_passenger/exceptions' +if !defined?(RUBY_ENGINE) || RUBY_ENGINE == "ruby" + require 'phusion_passenger/native_support' end -module Passenger +module PhusionPassenger # Utility functions. module Utils @@ -38,32 +40,33 @@ module Utils # and it correctly respects symbolic links. # # Raises SystemCallError if something went wrong. Raises ArgumentError - # if +path+ is nil. + # if +path+ is nil. Raises InvalidPath if +path+ does not appear + # to be a valid path. def normalize_path(path) raise ArgumentError, "The 'path' argument may not be nil" if path.nil? return Pathname.new(path).realpath.to_s rescue Errno::ENOENT => e - raise ArgumentError, e.message + raise InvalidAPath, e.message end # Assert that +app_root+ is a valid Ruby on Rails application root. - # Raises ArgumentError if that is not the case. + # Raises InvalidPath if that is not the case. def assert_valid_app_root(app_root) assert_valid_directory(app_root) assert_valid_file("#{app_root}/config/environment.rb") end - # Assert that +path+ is a directory. Raises +ArgumentError+ if it isn't. + # Assert that +path+ is a directory. Raises +InvalidPath+ if it isn't. def assert_valid_directory(path) if !File.directory?(path) - raise ArgumentError, "'#{path}' is not a valid directory." + raise InvalidPath, "'#{path}' is not a valid directory." end end - # Assert that +path+ is a file. Raises +ArgumentError+ if it isn't. + # Assert that +path+ is a file. Raises +InvalidPath+ if it isn't. def assert_valid_file(path) if !File.file?(path) - raise ArgumentError, "'#{path}' is not a valid file." + raise InvalidPath, "'#{path}' is not a valid file." end end @@ -156,16 +159,37 @@ def print_exception(current_location, exception) # Fork a new process and run the given block inside the child process, just like # fork(). Unlike fork(), this method is safe, i.e. there's no way for the child # process to escape the block. Any uncaught exceptions in the child process will - # be printed to standard output, citing _current_location_ as the source. - def safe_fork(current_location) - return fork do + # be printed to standard output, citing +current_location+ as the source. + # Futhermore, the child process will exit by calling Kernel#exit!, thereby + # bypassing any at_exit or ensure blocks. + # + # If +double_fork+ is true, then the child process will fork and immediately exit. + # This technique can be used to avoid zombie processes, at the expense of not + # being able to waitpid() the second child. + def safe_fork(current_location = self.class, double_fork = false) + pid = fork + if pid.nil? begin - yield + if double_fork + pid2 = fork + if pid2.nil? + yield + end + else + yield + end rescue Exception => e - print_exception(current_location, e) + print_exception(current_location.to_s, e) ensure exit! end + else + if double_fork + Process.waitpid(pid) rescue nil + return pid + else + return pid + end end end @@ -176,7 +200,25 @@ def safe_fork(current_location) # Exceptions are not propagated, except for SystemExit. def report_app_init_status(channel) begin - yield + old_global_stderr = $stderr + old_stderr = STDERR + stderr_output = "" + tempfile = Tempfile.new('passenger-stderr') + tempfile.unlink + Object.send(:remove_const, 'STDERR') rescue nil + Object.const_set('STDERR', tempfile) + begin + yield + ensure + Object.send(:remove_const, 'STDERR') rescue nil + Object.const_set('STDERR', old_stderr) + $stderr = old_global_stderr + if tempfile + tempfile.rewind + stderr_output = tempfile.read + tempfile.close rescue nil + end + end channel.write('success') return true rescue StandardError, ScriptError, NoMemoryError => e @@ -185,9 +227,12 @@ def report_app_init_status(channel) end channel.write('exception') channel.write_scalar(marshal_exception(e)) + channel.write_scalar(stderr_output) return false - rescue SystemExit + rescue SystemExit => e channel.write('exit') + channel.write_scalar(marshal_exception(e)) + channel.write_scalar(stderr_output) raise end end @@ -208,15 +253,19 @@ def unmarshal_and_raise_errors(channel, app_type = "rails") status = args[0] if status == 'exception' child_exception = unmarshal_exception(channel.read_scalar) + stderr = channel.read_scalar #print_exception(self.class.to_s, child_exception) raise AppInitError.new( "Application '#{@app_root}' raised an exception: " << "#{child_exception.class} (#{child_exception.message})", child_exception, - app_type) + app_type, + stderr.empty? ? nil : stderr) elsif status == 'exit' + child_exception = unmarshal_exception(channel.read_scalar) + stderr = channel.read_scalar raise AppInitError.new("Application '#{@app_root}' exited during startup", - nil, app_type) + child_exception, app_type, stderr.empty? ? nil : stderr) end end @@ -265,9 +314,40 @@ def switch_to_user(user) return true end end + + def sanitize_spawn_options(options) + defaults = { + "lower_privilege" => true, + "lowest_user" => "nobody", + "environment" => "production", + "app_type" => "rails", + "spawn_method" => "smart-lv2", + "framework_spawner_timeout" => -1, + "app_spawner_timeout" => -1 + } + options = defaults.merge(options) + options["lower_privilege"] = options["lower_privilege"].to_s == "true" + options["framework_spawner_timeout"] = options["framework_spawner_timeout"].to_i + options["app_spawner_timeout"] = options["app_spawner_timeout"].to_i + return options + end + + # Returns the directory in which to store Phusion Passenger-specific + # temporary files. If +create+ is true, then this method creates the + # directory if it doesn't exist. + def passenger_tmpdir(create = true) + dir = ENV['PHUSION_PASSENGER_TMP'] + if dir.nil? || dir.empty? + dir = Dir.tmpdir + end + if create && !File.exist?(dir) + system("mkdir", "-p", "-m", "u=rwxs,g=wx,o=wx", dir) + end + return dir + end end -end # module Passenger +end # module PhusionPassenger class Exception def backtrace_string(current_location = nil) @@ -287,15 +367,34 @@ class ConditionVariable # amount of time. Returns true if this condition was signaled, false if a # timeout occurred. def timed_wait(mutex, secs) - require 'timeout' unless defined?(Timeout) - if secs > 0 - Timeout.timeout(secs) do - wait(mutex) + if secs > 100000000 + # NOTE: If one calls timeout() on FreeBSD 5 with an + # argument of more than 100000000, then MRI will become + # stuck in an infite loop, blocking all threads. It seems + # that MRI uses select() to implement sleeping. + # I think that a value of more than 100000000 overflows + # select()'s data structures, causing it to behave incorrectly. + # So we just make sure we can't sleep more than 100000000 + # seconds. + secs = 100000000 + end + if defined?(RUBY_ENGINE) && RUBY_ENGINE == "jruby" + if secs > 0 + return wait(mutex, secs) + else + return wait(mutex) end else - wait(mutex) + require 'timeout' unless defined?(Timeout) + if secs > 0 + Timeout.timeout(secs) do + wait(mutex) + end + else + wait(mutex) + end + return true end - return true rescue Timeout::Error return false end @@ -304,31 +403,99 @@ def timed_wait(mutex, secs) # amount of time. Raises Timeout::Error if the timeout has elapsed. def timed_wait!(mutex, secs) require 'timeout' unless defined?(Timeout) - if secs > 0 - Timeout.timeout(secs) do + if secs > 100000000 + # See the corresponding note for timed_wait(). + secs = 100000000 + end + if defined?(RUBY_ENGINE) && RUBY_ENGINE == "jruby" + if secs > 0 + if !wait(mutex, secs) + raise Timeout::Error, "Timeout" + end + else wait(mutex) end else - wait(mutex) + if secs > 0 + Timeout.timeout(secs) do + wait(mutex) + end + else + wait(mutex) + end end + return nil end end class IO - # Send an IO object (i.e. a file descriptor) over this IO channel. - # This only works if this IO channel is a Unix socket. - # - # Raises SystemCallError if something went wrong. - def send_io(io) - Passenger::NativeSupport.send_fd(self.fileno, io.fileno) + if defined?(PhusionPassenger::NativeSupport) + # Send an IO object (i.e. a file descriptor) over this IO channel. + # This only works if this IO channel is a Unix socket. + # + # Raises SystemCallError if something went wrong. + def send_io(io) + PhusionPassenger::NativeSupport.send_fd(self.fileno, io.fileno) + end + + # Receive an IO object (i.e. a file descriptor) from this IO channel. + # This only works if this IO channel is a Unix socket. + # + # Raises SystemCallError if something went wrong. + def recv_io + return IO.new(PhusionPassenger::NativeSupport.recv_fd(self.fileno)) + end end - # Receive an IO object (i.e. a file descriptor) from this IO channel. - # This only works if this IO channel is a Unix socket. - # - # Raises SystemCallError if something went wrong. - def recv_io - return IO.new(Passenger::NativeSupport.recv_fd(self.fileno)) + def close_on_exec! + if defined?(Fcntl::F_SETFD) + fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) + end + end +end + +module Signal + # Like Signal.list, but only returns signals that we can actually trap. + def self.list_trappable + ruby_engine = defined?(RUBY_ENGINE) ? RUBY_ENGINE : "mri" + case ruby_engine + when "mri" + if RUBY_VERSION >= '1.9.0' + return Signal.list + else + result = Signal.list + result.delete("ALRM") + return result + end + when "jruby" + result = Signal.list + result.delete("QUIT") + result.delete("ILL") + result.delete("FPE") + result.delete("KILL") + result.delete("SEGV") + result.delete("STOP") + result.delete("USR1") + return result + else + return Signal.list + end + end +end + +# Ruby's implementation of UNIXSocket#recv_io and UNIXSocket#send_io +# are broken on 64-bit FreeBSD 7. So we override them with our own +# implementation. +if RUBY_PLATFORM =~ /freebsd/ + require 'socket' + UNIXSocket.class_eval do + def recv_io + super + end + + def send_io(io) + super + end end end @@ -341,4 +508,3 @@ def self.copy_on_write_friendly? end end end - diff --git a/lib/passenger/wsgi/application_spawner.rb b/lib/phusion_passenger/wsgi/application_spawner.rb similarity index 76% rename from lib/passenger/wsgi/application_spawner.rb rename to lib/phusion_passenger/wsgi/application_spawner.rb index 9d38a8e7..1e24bbb9 100644 --- a/lib/passenger/wsgi/application_spawner.rb +++ b/lib/phusion_passenger/wsgi/application_spawner.rb @@ -15,10 +15,10 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. require 'socket' -require 'passenger/application' -require 'passenger/message_channel' -require 'passenger/utils' -module Passenger +require 'phusion_passenger/application' +require 'phusion_passenger/message_channel' +require 'phusion_passenger/utils' +module PhusionPassenger module WSGI # Class for spawning WSGI applications. @@ -41,24 +41,26 @@ def self.spawn_application(*args) # - SystemCallError, IOError, SocketError: Something went wrong. def spawn_application(app_root, lower_privilege = true, lowest_user = "nobody", environment = "production") a, b = UNIXSocket.pair - # Double fork in order to prevent zombie processes. - pid = safe_fork(self.class.to_s) do - safe_fork(self.class.to_s) do - a.close - run(MessageChannel.new(b), app_root, lower_privilege, lowest_user, environment) - end + pid = safe_fork(self.class.to_s, true) do + a.close + + file_descriptors_to_leave_open = [0, 1, 2, b.fileno] + NativeSupport.close_all_file_descriptors(file_descriptors_to_leave_open) + close_all_io_objects_for_fds(file_descriptors_to_leave_open) + + run(MessageChannel.new(b), app_root, lower_privilege, lowest_user, environment) end b.close Process.waitpid(pid) rescue nil channel = MessageChannel.new(a) - pid, socket_name, using_abstract_namespace = channel.read + pid, socket_name, socket_type = channel.read if pid.nil? raise IOError, "Connection closed" end owner_pipe = channel.recv_io return Application.new(@app_root, pid, socket_name, - using_abstract_namespace == "true", owner_pipe) + socket_type, owner_pipe) end private @@ -70,11 +72,11 @@ def run(channel, app_root, lower_privilege, lowest_user, environment) lower_privilege('passenger_wsgi.py', lowest_user) end - socket_file = "/tmp/passenger_wsgi.#{Process.pid}.#{rand 10000000}" + socket_file = "#{passenger_tmpdir}/passenger_wsgi.#{Process.pid}.#{rand 10000000}" server = UNIXServer.new(socket_file) begin reader, writer = IO.pipe - channel.write(Process.pid, socket_file, "false") + channel.write(Process.pid, socket_file, "unix") channel.send_io(writer) writer.close channel.close @@ -84,6 +86,7 @@ def run(channel, app_root, lower_privilege, lowest_user, environment) exec(REQUEST_HANDLER, socket_file, server.fileno.to_s, reader.fileno.to_s) rescue + server.close File.unlink(socket_file) raise end @@ -91,4 +94,4 @@ def run(channel, app_root, lower_privilege, lowest_user, environment) end end # module WSGI -end # module Passenger +end # module PhusionPassenger diff --git a/lib/passenger/wsgi/request_handler.py b/lib/phusion_passenger/wsgi/request_handler.py similarity index 94% rename from lib/passenger/wsgi/request_handler.py rename to lib/phusion_passenger/wsgi/request_handler.py index ab37d932..cca1392a 100755 --- a/lib/passenger/wsgi/request_handler.py +++ b/lib/phusion_passenger/wsgi/request_handler.py @@ -30,7 +30,10 @@ def main_loop(self): try: env, input_stream = self.parse_request(client) if env: - self.process_request(env, input_stream, client) + if env['REQUEST_METHOD'] == 'ping': + self.process_ping(env, input_stream, client) + else: + self.process_request(env, input_stream, client) else: done = True except KeyboardInterrupt: @@ -147,6 +150,9 @@ def start_response(status, response_headers, exc_info = None): finally: if hasattr(result, 'close'): result.close() + + def process_ping(self, env, input_stream, output_stream): + output_stream.send("pong") def import_error_handler(environ, start_response): write = start_response('500 Import Error', [('Content-type', 'text/plain')]) diff --git a/man/passenger-memory-stats.8 b/man/passenger-memory-stats.8 index 5e3d0a69..10e83641 100644 --- a/man/passenger-memory-stats.8 +++ b/man/passenger-memory-stats.8 @@ -1,4 +1,4 @@ -.TH "passenger-memory-stats" "1" "2.0" "Phusion Passenger" "Administration Commands" +.TH "passenger-memory-stats" "8" "2.0" "Phusion Passenger" "Administration Commands" .SH "NAME" .LP passenger\-memory\-stats \- reports a snapshot of the Apache and Phusion Passenger memory statistcs diff --git a/misc/render_error_pages.rb b/misc/render_error_pages.rb index 2ddaca1c..e900f4e4 100755 --- a/misc/render_error_pages.rb +++ b/misc/render_error_pages.rb @@ -19,7 +19,7 @@ require 'passenger/html_template' require 'passenger/spawn_manager' require 'passenger/platform_info' -include Passenger +include PhusionPassenger if !defined?(Mysql::Error) module Mysql diff --git a/test/ApplicationPoolServerTest.cpp b/test/ApplicationPoolServerTest.cpp index f9ad84f1..7c55a15a 100644 --- a/test/ApplicationPoolServerTest.cpp +++ b/test/ApplicationPoolServerTest.cpp @@ -8,32 +8,11 @@ using namespace Passenger; namespace tut { - static bool firstRun = true; - static unsigned int initialFileDescriptors; - - static unsigned int countOpenFileDescriptors() { - int ret; - unsigned int result = 0; - for (long i = sysconf(_SC_OPEN_MAX) - 1; i >= 0; i--) { - do { - ret = dup2(i, i); - } while (ret == -1 && errno == EINTR); - if (ret != -1) { - result++; - } - } - return result; - } - struct ApplicationPoolServerTest { ApplicationPoolServerPtr server; ApplicationPoolPtr pool, pool2; ApplicationPoolServerTest() { - if (firstRun) { - initialFileDescriptors = countOpenFileDescriptors(); - firstRun = false; - } server = ptr(new ApplicationPoolServer( "../ext/apache2/ApplicationPoolServerExecutable", "stub/spawn_server.rb")); @@ -91,12 +70,5 @@ namespace tut { } } } - - TEST_METHOD(5) { - // ApplicationPoolServer should not leak file descriptors after running all - // of the above tests. - server = ApplicationPoolServerPtr(); - ensure_equals(countOpenFileDescriptors(), initialFileDescriptors); - } } diff --git a/test/ApplicationPoolServer_ApplicationPoolTest.cpp b/test/ApplicationPoolServer_ApplicationPoolTest.cpp index 94dd9863..42aa6e89 100644 --- a/test/ApplicationPoolServer_ApplicationPoolTest.cpp +++ b/test/ApplicationPoolServer_ApplicationPoolTest.cpp @@ -17,6 +17,10 @@ namespace tut { pool = server->connect(); pool2 = server->connect(); } + + ApplicationPoolPtr newPoolConnection() { + return server->connect(); + } }; DEFINE_TEST_GROUP(ApplicationPoolServer_ApplicationPoolTest); diff --git a/test/ApplicationPoolTest.cpp b/test/ApplicationPoolTest.cpp index 5dc6a6ff..634f0612 100644 --- a/test/ApplicationPoolTest.cpp +++ b/test/ApplicationPoolTest.cpp @@ -4,7 +4,9 @@ #include #include #include +#include #include +#include /** * This file is used as a template to test the different ApplicationPool implementations. @@ -12,6 +14,18 @@ */ #ifdef USE_TEMPLATE + struct TempFile { + const char *filename; + + TempFile(const char *filename) { + this->filename = filename; + } + + ~TempFile() { + unlink(filename); + } + }; + static string createRequestHeaders(const char *uri = "/foo/new") { string headers; #define ADD_HEADER(name, value) \ @@ -24,6 +38,7 @@ ADD_HEADER("REQUEST_URI", uri); ADD_HEADER("REQUEST_METHOD", "GET"); ADD_HEADER("REMOTE_ADDR", "localhost"); + ADD_HEADER("PATH_INFO", uri); return headers; } @@ -47,11 +62,17 @@ } static Application::SessionPtr spawnRackApp(ApplicationPoolPtr pool, const char *appRoot) { - return pool->get(appRoot, true, "nobody", "production", "smart", "rack"); + PoolOptions options; + options.appRoot = appRoot; + options.appType = "rack"; + return pool->get(options); } static Application::SessionPtr spawnWsgiApp(ApplicationPoolPtr pool, const char *appRoot) { - return pool->get(appRoot, true, "nobody", "production", "smart", "wsgi"); + PoolOptions options; + options.appRoot = appRoot; + options.appType = "wsgi"; + return pool->get(options); } TEST_METHOD(1) { @@ -69,7 +90,7 @@ TEST_METHOD(2) { // Verify that the pool spawns a new app, and that // after the session is closed, the app is kept around. - Application::SessionPtr session(pool->get("stub/railsapp")); + Application::SessionPtr session(spawnRackApp(pool, "stub/rack")); ensure_equals("Before the session was closed, the app was busy", pool->getActive(), 1u); ensure_equals("Before the session was closed, the app was in the pool", pool->getCount(), 1u); session.reset(); @@ -81,17 +102,17 @@ // If we call get() with an application root, then we close the session, // and then we call get() again with the same application root, // then the pool should not have spawned more than 1 app in total. - Application::SessionPtr session(pool->get("stub/railsapp")); + Application::SessionPtr session(spawnRackApp(pool, "stub/rack")); session.reset(); - session = pool->get("stub/railsapp"); + session = spawnRackApp(pool, "stub/rack"); ensure_equals(pool->getCount(), 1u); } TEST_METHOD(4) { // If we call get() with an application root, then we call get() again before closing // the session, then the pool should have spawned 2 apps in total. - Application::SessionPtr session(pool->get("stub/railsapp")); - Application::SessionPtr session2(pool2->get("stub/railsapp")); + Application::SessionPtr session(spawnRackApp(pool, "stub/rack")); + Application::SessionPtr session2(spawnRackApp(pool, "stub/rack")); ensure_equals(pool->getCount(), 2u); } @@ -105,12 +126,12 @@ session->sendHeaders(createRequestHeaders()); string result(readAll(session->getStream())); - ensure("Session 1 belongs to the correct app", result.find("hello world")); + ensure("Session 1 belongs to the correct app", result.find("hello world") != string::npos); session.reset(); session2->sendHeaders(createRequestHeaders()); result = readAll(session2->getStream()); - ensure("Session 2 belongs to the correct app", result.find("this is railsapp2")); + ensure("Session 2 belongs to the correct app", result.find("this is railsapp2") != string::npos); session2.reset(); } @@ -246,8 +267,8 @@ } TEST_METHOD(12) { - // If tmp/restart.txt is present, then the applications under app_root - // should be restarted. + // If tmp/restart.txt didn't exist but has now been created, + // then the applications under app_root should be restarted. struct stat buf; Application::SessionPtr session1 = pool->get("stub/railsapp"); Application::SessionPtr session2 = pool2->get("stub/railsapp"); @@ -260,62 +281,46 @@ ensure_equals("No apps are active", pool->getActive(), 0u); ensure_equals("Both apps are killed, and a new one was spawned", pool->getCount(), 1u); - ensure("Restart file has been deleted", - stat("stub/railsapp/tmp/restart.txt", &buf) == -1 - && errno == ENOENT); + try { + ensure("Restart file still exists", + stat("stub/railsapp/tmp/restart.txt", &buf) == 0); + unlink("stub/railsapp/tmp/restart.txt"); + } catch (...) { + unlink("stub/railsapp/tmp/restart.txt"); + throw; + } } TEST_METHOD(13) { - // If tmp/restart.txt is present, but cannot be deleted, then - // the applications under app_root should still be restarted. - // However, a subsequent get() should not result in a restart. - pid_t old_pid, pid; - struct stat buf; - Application::SessionPtr session1 = pool->get("stub/railsapp"); - Application::SessionPtr session2 = pool2->get("stub/railsapp"); - session1.reset(); - session2.reset(); + // If tmp/restart.txt was present, and its timestamp changed + // since the last check, then the applications under app_root + // should still be restarted. However, a subsequent get() + // should not result in a restart. + pid_t old_pid; system("mkdir -p stub/railsapp/tmp/restart.txt"); - - old_pid = pool->get("stub/railsapp")->getPid(); try { - ensure("Restart file has not been deleted", - stat("stub/railsapp/tmp/restart.txt", &buf) == 0); - system("rmdir stub/railsapp/tmp/restart.txt"); + Application::SessionPtr session = pool->get("stub/railsapp"); + old_pid = session->getPid(); + session.reset(); + + struct utimbuf buf; + buf.actime = time(NULL) - 10; + buf.modtime = time(NULL) - 10; + utime("stub/railsapp/tmp/restart.txt", &buf); + + session = pool->get("stub/railsapp"); + ensure("The app was restarted", session->getPid() != old_pid); + old_pid = session->getPid(); + session.reset(); + + session = pool->get("stub/railsapp"); + ensure_equals("The app was not restarted", + old_pid, session->getPid()); } catch (...) { system("rmdir stub/railsapp/tmp/restart.txt"); throw; } - - pid = pool->get("stub/railsapp")->getPid(); - ensure_equals("The app was not restarted", pid, old_pid); - unlink("stub/railsapp/tmp/restart.txt"); - } - - TEST_METHOD(14) { - // If tmp/restart.txt is present, but cannot be deleted, then - // the applications under app_root should still be restarted. - // A subsequent get() should only restart if we've changed - // restart.txt's mtime. - pid_t old_pid; - Application::SessionPtr session1 = pool->get("stub/railsapp"); - Application::SessionPtr session2 = pool2->get("stub/railsapp"); - session1.reset(); - session2.reset(); - - setenv("nextRestartTxtDeletionShouldFail", "1", 1); - system("touch stub/railsapp/tmp/restart.txt"); - old_pid = pool->get("stub/railsapp")->getPid(); - ensure_equals(pool->getActive(), 0u); - ensure_equals(pool->getCount(), 1u); - - sleep(1); // Allow the next mtime to be different. - system("touch stub/railsapp/tmp/restart.txt"); - ensure("The app is restarted, and the last app instance was not reused", - pool2->get("stub/railsapp")->getPid() != old_pid); - - unlink("stub/railsapp/tmp/restart.txt"); } TEST_METHOD(15) { @@ -325,17 +330,24 @@ Application::SessionPtr session = pool->get("stub/railsapp"); session->sendHeaders(createRequestHeaders("/bar")); string result = readAll(session->getStream()); - ensure(result.find("bar 1!")); + ensure(result.find("bar 1!") != string::npos); session.reset(); - + system("cp -f stub/railsapp/app/controllers/bar_controller_2.rb " "stub/railsapp/app/controllers/bar_controller.rb"); system("touch stub/railsapp/tmp/restart.txt"); - session = pool->get("stub/railsapp"); - session->sendHeaders(createRequestHeaders("/bar")); - result = readAll(session->getStream()); - ensure("App code has been reloaded", result.find("bar 2!")); + try { + session = pool->get("stub/railsapp"); + session->sendHeaders(createRequestHeaders("/bar")); + result = readAll(session->getStream()); + ensure("App code has been reloaded", result.find("bar 2!") != string::npos); + } catch (...) { + unlink("stub/railsapp/app/controllers/bar_controller.rb"); + unlink("touch stub/railsapp/tmp/restart.txt"); + throw; + } unlink("stub/railsapp/app/controllers/bar_controller.rb"); + unlink("touch stub/railsapp/tmp/restart.txt"); } TEST_METHOD(16) { @@ -351,10 +363,55 @@ } TEST_METHOD(17) { - // MaxPerApp must be respected. + // MaxPerApp is respected. pool->setMax(3); pool->setMaxPerApp(1); - // TODO: how do we test this? + + // We connect to stub/rack while it already has an instance with + // 1 request in its queue. Assert that the pool doesn't spawn + // another instance. + Application::SessionPtr session1 = spawnRackApp(pool, "stub/rack"); + Application::SessionPtr session2 = spawnRackApp(pool2, "stub/rack"); + ensure_equals(pool->getCount(), 1u); + + // We connect to stub/wsgi. Assert that the pool spawns a new + // instance for this app. + ApplicationPoolPtr pool3(newPoolConnection()); + Application::SessionPtr session3 = spawnWsgiApp(pool3, "stub/wsgi"); + ensure_equals(pool->getCount(), 2u); + } + + TEST_METHOD(18) { + // Application instance is shutdown after 'maxRequests' requests. + PoolOptions options("stub/railsapp"); + int reader; + pid_t originalPid; + Application::SessionPtr session; + + options.maxRequests = 4; + pool->setMax(1); + session = pool->get(options); + originalPid = session->getPid(); + session.reset(); + + for (unsigned int i = 0; i < 4; i++) { + session = pool->get(options); + session->sendHeaders(createRequestHeaders()); + session->shutdownWriter(); + reader = session->getStream(); + readAll(reader); + // Must explicitly call reset() here because we + // want to close the session right now. + session.reset(); + // In case of ApplicationPoolServer, we sleep here + // for a little while to force a context switch to + // the server, so that the session close event may + // be processed. + usleep(100000); + } + + session = pool->get(options); + ensure(session->getPid() != originalPid); } struct SpawnRackAppFunction { @@ -362,7 +419,11 @@ bool *done; void operator()() { - spawnRackApp(pool, "stub/rack"); + PoolOptions options; + options.appRoot = "stub/rack"; + options.appType = "rack"; + options.useGlobalQueue = true; + pool->get(options); *done = true; } }; @@ -371,9 +432,13 @@ // If global queueing mode is enabled, then get() waits until // there's at least one idle backend process for this application // domain. - pool->setUseGlobalQueue(true); pool->setMax(1); - Application::SessionPtr session = spawnRackApp(pool, "stub/rack"); + + PoolOptions options; + options.appRoot = "stub/rack"; + options.appType = "rack"; + options.useGlobalQueue = true; + Application::SessionPtr session = pool->get(options); bool done = false; SpawnRackAppFunction func; @@ -391,6 +456,179 @@ thr.join(); } - // TODO: test maxIdleTime == 0 + TEST_METHOD(20) { + // If tmp/always_restart.txt is present, then the application under app_root + // should be always restarted. + try { + struct stat buf; + Application::SessionPtr session1 = pool->get("stub/railsapp"); + Application::SessionPtr session2 = pool2->get("stub/railsapp"); + session1.reset(); + session2.reset(); + + system("touch stub/railsapp/tmp/always_restart.txt"); + pool->get("stub/railsapp"); + + ensure_equals("No apps are active", pool->getActive(), 0u); + ensure_equals("Both apps are killed, and a new one was spawned", + pool->getCount(), 1u); + ensure("always_restart file has not been deleted", + stat("stub/railsapp/tmp/always_restart.txt", &buf) == 0); + unlink("stub/railsapp/tmp/always_restart.txt"); + } catch (...) { + unlink("stub/railsapp/tmp/always_restart.txt"); + throw; + } + } + + TEST_METHOD(21) { + // If tmp/always_restart.txt is present and is a directory, + // then the application under app_root + // should be always restarted. + try { + struct stat buf; + Application::SessionPtr session1 = pool->get("stub/railsapp"); + Application::SessionPtr session2 = pool2->get("stub/railsapp"); + session1.reset(); + session2.reset(); + + system("mkdir stub/railsapp/tmp/always_restart.txt"); + pool->get("stub/railsapp"); + + ensure_equals("No apps are active", pool->getActive(), 0u); + ensure_equals("Both apps are killed, and a new one was spawned", + pool->getCount(), 1u); + ensure("always_restart file has not been deleted", + stat("stub/railsapp/tmp/always_restart.txt", &buf) == 0); + system("rmdir stub/railsapp/tmp/always_restart.txt"); + } catch (...) { + system("rmdir stub/railsapp/tmp/always_restart.txt"); + throw; + } + } + + TEST_METHOD(22) { + // Test whether tmp/always_restart.txt really results in code reload. + try { + system("cp -f stub/railsapp/app/controllers/bar_controller_1.rb " + "stub/railsapp/app/controllers/bar_controller.rb"); + Application::SessionPtr session = pool->get("stub/railsapp"); + session->sendHeaders(createRequestHeaders("/bar")); + string result = readAll(session->getStream()); + ensure(result.find("bar 1!") != string::npos); + session.reset(); + + system("cp -f stub/railsapp/app/controllers/bar_controller_2.rb " + "stub/railsapp/app/controllers/bar_controller.rb"); + system("touch stub/railsapp/tmp/always_restart.txt"); + session = pool->get("stub/railsapp"); + session->sendHeaders(createRequestHeaders("/bar")); + result = readAll(session->getStream()); + ensure("App code has been reloaded (1)", result.find("bar 2!") != string::npos); + session.reset(); + + system("cp -f stub/railsapp/app/controllers/bar_controller_1.rb " + "stub/railsapp/app/controllers/bar_controller.rb"); + session = pool->get("stub/railsapp"); + session->sendHeaders(createRequestHeaders("/bar")); + result = readAll(session->getStream()); + ensure("App code has been reloaded (2)", result.find("bar 1!") != string::npos); + session.reset(); + + unlink("stub/railsapp/app/controllers/bar_controller.rb"); + unlink("stub/railsapp/tmp/always_restart.txt"); + } catch (...) { + unlink("stub/railsapp/app/controllers/bar_controller.rb"); + unlink("stub/railsapp/tmp/always_restart.txt"); + throw; + } + } + + TEST_METHOD(23) { + // If tmp/restart.txt and tmp/always_restart.txt are present, + // the application under app_root should still be restarted and + // both files must be kept + try { + pid_t old_pid, pid; + struct stat buf; + Application::SessionPtr session1 = pool->get("stub/railsapp"); + Application::SessionPtr session2 = pool2->get("stub/railsapp"); + session1.reset(); + session2.reset(); + + system("touch stub/railsapp/tmp/restart.txt"); + system("touch stub/railsapp/tmp/always_restart.txt"); + + old_pid = pool->get("stub/railsapp")->getPid(); + ensure("always_restart file has not been deleted", + stat("stub/railsapp/tmp/always_restart.txt", &buf) == 0); + + ensure("Restart file has not been deleted", + stat("stub/railsapp/tmp/restart.txt", &buf) == 0); + + pid = pool->get("stub/railsapp")->getPid(); + ensure("The app was restarted", pid != old_pid); + unlink("stub/railsapp/tmp/restart.txt"); + unlink("stub/railsapp/tmp/always_restart.txt"); + } catch (...) { + unlink("stub/railsapp/tmp/restart.txt"); + unlink("stub/railsapp/tmp/always_restart.txt"); + throw; + } + } + + TEST_METHOD(24) { + // It should look for restart.txt in the directory given by + // the restartDir option, if available. + struct stat buf; + char path[1024]; + PoolOptions options("stub/rack"); + options.appType = "rack"; + options.restartDir = string(getcwd(path, sizeof(path))) + "/stub/rack"; + + Application::SessionPtr session1 = pool->get(options); + Application::SessionPtr session2 = pool2->get(options); + session1.reset(); + session2.reset(); + + TempFile tempfile("stub/rack/restart.txt"); + system("touch stub/rack/restart.txt"); + + pool->get(options); + + ensure_equals("No apps are active", pool->getActive(), 0u); + ensure_equals("Both apps are killed, and a new one was spawned", + pool->getCount(), 1u); + ensure("Restart file still exists", + stat("stub/rack/restart.txt", &buf) == 0); + } + + TEST_METHOD(25) { + // restartDir may also be a directory relative to the + // application root. + struct stat buf; + PoolOptions options("stub/rack"); + options.appType = "rack"; + options.restartDir = "public"; + + Application::SessionPtr session1 = pool->get(options); + Application::SessionPtr session2 = pool2->get(options); + session1.reset(); + session2.reset(); + + TempFile tempfile("stub/rack/public/restart.txt"); + system("touch stub/rack/public/restart.txt"); + + pool->get(options); + + ensure_equals("No apps are active", pool->getActive(), 0u); + ensure_equals("Both apps are killed, and a new one was spawned", + pool->getCount(), 1u); + ensure("Restart file still exists", + stat("stub/rack/public/restart.txt", &buf) == 0); + } + + /*************************************/ + #endif /* USE_TEMPLATE */ diff --git a/test/CachedFileStatTest.cpp b/test/CachedFileStatTest.cpp new file mode 100644 index 00000000..f08d9410 --- /dev/null +++ b/test/CachedFileStatTest.cpp @@ -0,0 +1,262 @@ +#include "tut.h" +#include "CachedFileStat.h" +#include "SystemTime.h" +#include +#include + +using namespace std; +using namespace Passenger; + +namespace tut { + struct CachedFileStatTest { + CachedFileStat *stat; + CachedMultiFileStat *mstat; + + CachedFileStatTest() { + stat = (CachedFileStat *) NULL; + mstat = (CachedMultiFileStat *) NULL; + } + + ~CachedFileStatTest() { + if (stat != NULL) { + delete stat; + } + if (mstat != NULL) { + cached_multi_file_stat_free(mstat); + } + SystemTime::release(); + unlink("test.txt"); + unlink("test2.txt"); + unlink("test3.txt"); + unlink("test4.txt"); + } + }; + + static void touch(const char *filename, time_t timestamp = 0) { + FILE *f = fopen(filename, "w"); + fprintf(f, "hi"); + fclose(f); + if (timestamp != 0) { + struct utimbuf buf; + buf.actime = timestamp; + buf.modtime = timestamp; + utime(filename, &buf); + } + } + + DEFINE_TEST_GROUP(CachedFileStatTest); + + /************ Tests for CachedFileStat ************/ + + TEST_METHOD(1) { + // cached_file_stat_new() does not stat the file immediately. + touch("test.txt"); + stat = new CachedFileStat("test.txt"); + ensure_equals((long) stat->info.st_size, (long) 0); + ensure_equals(stat->info.st_mtime, (time_t) 0); + } + + TEST_METHOD(2) { + // cached_file_stat_refresh() on a newly created + // CachedFileStat works. + touch("test.txt"); + stat = new CachedFileStat("test.txt"); + ensure_equals(stat->refresh(1), 0); + ensure_equals((long) stat->info.st_size, (long) 2); + } + + TEST_METHOD(3) { + // cached_file_stat_refresh() does not re-stat the file + // until the cache has expired. + SystemTime::force(5); + stat = new CachedFileStat("test.txt"); + touch("test.txt", 1); + ensure_equals("1st refresh succceeded", + stat->refresh(1), + 0); + + touch("test.txt", 1000); + ensure_equals("2nd refresh succceeded", + stat->refresh(1), + 0); + ensure_equals("Cached value was used", + stat->info.st_mtime, + (time_t) 1); + + SystemTime::force(6); + ensure_equals("3rd refresh succceeded", + stat->refresh(1), + 0); + ensure_equals("Cache has been invalidated", + stat->info.st_mtime, + (time_t) 1000); + } + + TEST_METHOD(5) { + // cached_file_stat_refresh() on a nonexistant file returns + // an error. + stat = new CachedFileStat("test.txt"); + ensure_equals(stat->refresh(1), -1); + ensure_equals("It sets errno appropriately", errno, ENOENT); + } + + TEST_METHOD(6) { + // cached_file_stat_refresh() on a nonexistant file does not + // re-stat the file until the cache has expired. + SystemTime::force(5); + stat = new CachedFileStat("test.txt"); + ensure_equals("1st refresh failed", + stat->refresh(1), + -1); + ensure_equals("It sets errno appropriately", errno, ENOENT); + + errno = EEXIST; + ensure_equals("2nd refresh failed", + stat->refresh(1), + -1); + ensure_equals("It sets errno appropriately", errno, ENOENT); + ensure_equals("Cached value was used", + stat->info.st_mtime, + (time_t) 0); + + touch("test.txt", 1000); + SystemTime::force(6); + ensure_equals("3rd refresh succeeded", + stat->refresh(1), + 0); + ensure_equals("Cache has been invalidated", + stat->info.st_mtime, + (time_t) 1000); + + unlink("test.txt"); + ensure_equals("4th refresh succeeded even though file was unlinked", + stat->refresh(1), + 0); + ensure_equals("Cached value was used", + stat->info.st_mtime, + (time_t) 1000); + } + + + /************ Tests for CachedMultiFileStat ************/ + + TEST_METHOD(10) { + // Statting an existing file works. + struct stat buf; + + touch("test.txt"); + mstat = cached_multi_file_stat_new(1); + ensure_equals( + cached_multi_file_stat_perform(mstat, "test.txt", &buf, 0), + 0); + ensure_equals((long) buf.st_size, (long) 2); + } + + TEST_METHOD(11) { + // Statting a nonexistant file works. + struct stat buf; + + mstat = cached_multi_file_stat_new(1); + ensure_equals( + cached_multi_file_stat_perform(mstat, "test.txt", &buf, 0), + -1); + } + + TEST_METHOD(12) { + // Throttling works. + struct stat buf; + + mstat = cached_multi_file_stat_new(2); + SystemTime::force(5); + + // Touch and stat test.txt. The next stat should return + // the old info. + + touch("test.txt", 10); + ensure_equals( + cached_multi_file_stat_perform(mstat, "test.txt", &buf, 1), + 0); + ensure_equals(buf.st_mtime, (time_t) 10); + + touch("test.txt", 20); + ensure_equals( + cached_multi_file_stat_perform(mstat, "test.txt", &buf, 1), + 0); + ensure_equals(buf.st_mtime, (time_t) 10); + + // Touch and stat test2.txt. The next stat should return + // the old info. + + touch("test2.txt", 30); + ensure_equals( + cached_multi_file_stat_perform(mstat, "test2.txt", &buf, 1), + 0); + ensure_equals(buf.st_mtime, (time_t) 30); + + touch("test2.txt", 40); + ensure_equals( + cached_multi_file_stat_perform(mstat, "test2.txt", &buf, 1), + 0); + ensure_equals(buf.st_mtime, (time_t) 30); + + // Forward timer, then stat both files again. The most recent + // information should be returned. + + SystemTime::force(6); + ensure_equals( + cached_multi_file_stat_perform(mstat, "test.txt", &buf, 1), + 0); + ensure_equals(buf.st_mtime, (time_t) 20); + ensure_equals( + cached_multi_file_stat_perform(mstat, "test2.txt", &buf, 1), + 0); + ensure_equals(buf.st_mtime, (time_t) 40); + } + + TEST_METHOD(13) { + // Cache limiting works. + struct stat buf; + + mstat = cached_multi_file_stat_new(3); + SystemTime::force(5); + + // Create and stat test.txt, test2.txt and test3.txt. + + touch("test.txt", 1000); + ensure_equals( + cached_multi_file_stat_perform(mstat, "test.txt", &buf, 1), + 0); + ensure_equals(buf.st_mtime, (time_t) 1000); + + touch("test2.txt", 1001); + ensure_equals( + cached_multi_file_stat_perform(mstat, "test2.txt", &buf, 1), + 0); + ensure_equals(buf.st_mtime, (time_t) 1001); + + touch("test3.txt", 1003); + ensure_equals( + cached_multi_file_stat_perform(mstat, "test3.txt", &buf, 1), + 0); + ensure_equals(buf.st_mtime, (time_t) 1003); + + // Stat test2.txt, then create and stat test4.txt, then touch test.txt. + // test.txt should have been removed from the cache, and thus + // upon statting it again its new timestamp should be returned. + + ensure_equals( + cached_multi_file_stat_perform(mstat, "test2.txt", &buf, 1), + 0); + + touch("test4.txt", 1004); + ensure_equals( + cached_multi_file_stat_perform(mstat, "test4.txt", &buf, 1), + 0); + + touch("test.txt", 3000); + ensure_equals( + cached_multi_file_stat_perform(mstat, "test.txt", &buf, 1), + 0); + ensure_equals(buf.st_mtime, (time_t) 3000); + } +} diff --git a/test/FileCheckerTest.cpp b/test/FileCheckerTest.cpp new file mode 100644 index 00000000..4eef5539 --- /dev/null +++ b/test/FileCheckerTest.cpp @@ -0,0 +1,79 @@ +#include "tut.h" +#include "FileChecker.h" +#include "SystemTime.h" +#include +#include + +using namespace Passenger; +using namespace std; + +namespace tut { + struct FileCheckerTest { + FileCheckerTest() { + } + + ~FileCheckerTest() { + unlink("test.txt"); + SystemTime::release(); + } + }; + + static void touch(const char *filename, time_t timestamp = 0) { + FILE *f = fopen(filename, "w"); + fclose(f); + if (timestamp != 0) { + struct utimbuf buf; + buf.actime = timestamp; + buf.modtime = timestamp; + utime(filename, &buf); + } + } + + DEFINE_TEST_GROUP(FileCheckerTest); + + TEST_METHOD(1) { + // File is not changed if it didn't exist. + FileChecker checker("test.txt"); + ensure(!checker.changed()); + } + + TEST_METHOD(2) { + // File is not changed if its ctime and mtime didn't change. + touch("test.txt"); + FileChecker checker("test.txt"); + ensure(!checker.changed()); + } + + TEST_METHOD(3) { + // File is changed if it didn't exist but has now been created. + FileChecker checker("test.txt"); + touch("test.txt"); + ensure(checker.changed()); + } + + TEST_METHOD(4) { + // File is changed if its mtime changed. + touch("test.txt", time(NULL) - 5); + FileChecker checker("test.txt"); + touch("test.txt"); + ensure("First check: changed", checker.changed()); + ensure("Second check: unchanged", !checker.changed()); + } + + TEST_METHOD(5) { + // Throttling works. + SystemTime::force(5); + + FileChecker checker("test.txt"); + checker.changed(3); + touch("test.txt"); + ensure(!checker.changed(3)); + + SystemTime::force(6); + ensure(!checker.changed(3)); + + SystemTime::force(8); + ensure(checker.changed(3)); + ensure(!checker.changed(3)); + } +} diff --git a/test/MessageChannelTest.cpp b/test/MessageChannelTest.cpp index 198731a8..51118a0c 100644 --- a/test/MessageChannelTest.cpp +++ b/test/MessageChannelTest.cpp @@ -91,7 +91,7 @@ namespace tut { close(p1[1]); close(p2[0]); close(p2[1]); - execlp("ruby", "ruby", "./stub/message_channel.rb", NULL); + execlp("ruby", "ruby", "./stub/message_channel.rb", (char *) 0); perror("Cannot execute ruby"); _exit(1); } else { @@ -203,7 +203,7 @@ namespace tut { close(p1[1]); close(p2[0]); close(p2[1]); - execlp("ruby", "ruby", "./stub/message_channel_2.rb", NULL); + execlp("ruby", "ruby", "./stub/message_channel_2.rb", (void *) 0); perror("Cannot execute ruby"); _exit(1); } else { @@ -245,7 +245,7 @@ namespace tut { dup2(fd[0], 3); close(fd[0]); close(fd[1]); - execlp("ruby", "ruby", "./stub/message_channel_3.rb", NULL); + execlp("ruby", "ruby", "./stub/message_channel_3.rb", (void *) 0); perror("Cannot execute ruby"); _exit(1); } else { diff --git a/test/PoolOptionsTest.cpp b/test/PoolOptionsTest.cpp new file mode 100644 index 00000000..5f5231c6 --- /dev/null +++ b/test/PoolOptionsTest.cpp @@ -0,0 +1,37 @@ +#include "tut.h" +#include "PoolOptions.h" + +using namespace Passenger; +using namespace std; + +namespace tut { + struct PoolOptionsTest { + }; + + DEFINE_TEST_GROUP(PoolOptionsTest); + + // Test the PoolOptions constructors and toVector(). + TEST_METHOD(1) { + PoolOptions options; + options.appRoot = "/foo"; + options.frameworkSpawnerTimeout = 123; + options.appSpawnerTimeout = 456; + options.maxRequests = 789; + + vector args; + args.push_back("abc"); + args.push_back("def"); + options.toVector(args); + + PoolOptions copy(args, 2); + ensure_equals(options.appRoot, copy.appRoot); + ensure_equals(options.lowerPrivilege, copy.lowerPrivilege); + ensure_equals(options.lowestUser, copy.lowestUser); + ensure_equals(options.environment, copy.environment); + ensure_equals(options.spawnMethod, copy.spawnMethod); + ensure_equals(options.appType, copy.appType); + ensure_equals(options.frameworkSpawnerTimeout, copy.frameworkSpawnerTimeout); + ensure_equals(options.appSpawnerTimeout, copy.appSpawnerTimeout); + ensure_equals(options.maxRequests, copy.maxRequests); + } +} diff --git a/test/SpawnManagerTest.cpp b/test/SpawnManagerTest.cpp index d11901b2..226f8d27 100644 --- a/test/SpawnManagerTest.cpp +++ b/test/SpawnManagerTest.cpp @@ -19,7 +19,7 @@ namespace tut { TEST_METHOD(1) { // Spawning an application should return a valid Application object. - ApplicationPtr app(manager.spawn(".")); + ApplicationPtr app(manager.spawn(PoolOptions("."))); ensure_equals("The Application object's PID is the same as the one specified by the stub", app->getPid(), 1234); } @@ -32,7 +32,7 @@ namespace tut { // Give the spawn server the time to properly terminate. usleep(500000); - ApplicationPtr app(manager.spawn(".")); + ApplicationPtr app(manager.spawn(PoolOptions("."))); ensure_equals("The Application object's PID is the same as the one specified by the stub", app->getPid(), 1234); @@ -51,10 +51,10 @@ namespace tut { kill(manager.getServerPid(), SIGTERM); // Give the spawn server the time to properly terminate. usleep(500000); - + try { manager.nextRestartShouldFail = true; - ApplicationPtr app(manager.spawn(".")); + ApplicationPtr app(manager.spawn(PoolOptions("."))); fail("SpawnManager did not throw a SpawnException"); } catch (const SpawnException &e) { // Success. diff --git a/test/StandardApplicationPoolTest.cpp b/test/StandardApplicationPoolTest.cpp index 22d0cd78..83475fee 100644 --- a/test/StandardApplicationPoolTest.cpp +++ b/test/StandardApplicationPoolTest.cpp @@ -12,6 +12,10 @@ namespace tut { pool = ptr(new StandardApplicationPool("../bin/passenger-spawn-server")); pool2 = pool; } + + ApplicationPoolPtr newPoolConnection() { + return pool; + } }; DEFINE_TEST_GROUP(StandardApplicationPoolTest); diff --git a/test/SystemTimeTest.cpp b/test/SystemTimeTest.cpp new file mode 100644 index 00000000..b6eeb321 --- /dev/null +++ b/test/SystemTimeTest.cpp @@ -0,0 +1,37 @@ +#include "tut.h" +#include "SystemTime.h" + +using namespace Passenger; +using namespace std; + +namespace tut { + struct SystemTimeTest { + ~SystemTimeTest() { + SystemTime::release(); + } + }; + + DEFINE_TEST_GROUP(SystemTimeTest); + + TEST_METHOD(1) { + time_t begin = SystemTime::get(); + + SystemTime::force(1); + ensure_equals(SystemTime::get(), (time_t) 1); + SystemTime::release(); + + time_t now = SystemTime::get(); + ensure(now >= begin && now <= begin + 2); + } + + TEST_METHOD(2) { + time_t begin = SystemTime::get(); + + SystemTime::force(1); + ensure_equals(SystemTime::get(), (time_t) 1); + SystemTime::release(); + + time_t now = SystemTime::get(); + ensure(now >= begin && now <= begin + 2); + } +} diff --git a/test/UtilsTest.cpp b/test/UtilsTest.cpp index bf725823..8f76c7f7 100644 --- a/test/UtilsTest.cpp +++ b/test/UtilsTest.cpp @@ -1,5 +1,9 @@ #include "tut.h" #include "Utils.h" +#include +#include +#include +#include #include #include @@ -13,12 +17,31 @@ namespace tut { UtilsTest() { oldPath = getenv("PATH"); + unsetenv("TMPDIR"); + unsetenv("PHUSION_PASSENGER_TMP"); } ~UtilsTest() { setenv("PATH", oldPath.c_str(), 1); + unsetenv("TMPDIR"); + unsetenv("PHUSION_PASSENGER_TMP"); } }; + + static vector + listDir(const char *path) { + vector result; + DIR *d = opendir(path); + struct dirent *ent; + + while ((ent = readdir(d)) != NULL) { + if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) { + continue; + } + result.push_back(ent->d_name); + } + return result; + } DEFINE_TEST_GROUP(UtilsTest); @@ -98,4 +121,118 @@ namespace tut { setenv("PATH", binpath.c_str(), 1); ensure("Spawn server is found.", !findSpawnServer().empty()); } + + + /***** Test getTempDir() *****/ + + TEST_METHOD(11) { + // It returns "/tmp" if the TMPDIR environment is NULL. + ensure_equals(string(getTempDir()), "/tmp"); + } + + TEST_METHOD(12) { + // It returns "/tmp" if the TMPDIR environment is an empty string. + setenv("TMPDIR", "", 1); + ensure_equals(string(getTempDir()), "/tmp"); + } + + TEST_METHOD(13) { + // It returns the value of the TMPDIR environment if it is not NULL and not empty. + setenv("TMPDIR", "/foo", 1); + ensure_equals(string(getTempDir()), "/foo"); + } + + + /***** Test getPassengerTempDir() *****/ + + TEST_METHOD(15) { + // It returns "(tempdir)/passenger.(pid)" + char dir[128]; + + snprintf(dir, sizeof(dir), "/tmp/passenger.%lu", (unsigned long) getpid()); + ensure_equals(getPassengerTempDir(), dir); + } + + TEST_METHOD(16) { + // It caches the result into the PHUSION_PASSENGER_TMP environment variable. + char dir[128]; + + snprintf(dir, sizeof(dir), "/tmp/passenger.%lu", (unsigned long) getpid()); + getPassengerTempDir(); + ensure_equals(getenv("PHUSION_PASSENGER_TMP"), string(dir)); + } + + TEST_METHOD(17) { + // It returns the value of the PHUSION_PASSENGER_TMP environment variable if it's not NULL and not an empty string. + setenv("PHUSION_PASSENGER_TMP", "/foo", 1); + ensure_equals(getPassengerTempDir(), "/foo"); + } + + TEST_METHOD(18) { + // It does not use query the PHUSION_PASSENGER_TMP environment variable if bypassCache is true. + char dir[128]; + + setenv("PHUSION_PASSENGER_TMP", "/foo", 1); + snprintf(dir, sizeof(dir), "/tmp/passenger.%lu", (unsigned long) getpid()); + ensure_equals(getPassengerTempDir(true), dir); + } + + + /***** Test TempFile *****/ + + TEST_METHOD(20) { + // It creates a temp file inside getPassengerTempDir(). + setenv("PHUSION_PASSENGER_TMP", "utils_test.tmp", 1); + mkdir("utils_test.tmp", S_IRWXU); + TempFile t("temp", false); + unsigned int size = listDir("utils_test.tmp").size(); + removeDirTree("utils_test.tmp"); + ensure_equals(size, 1u); + } + + TEST_METHOD(21) { + // It deletes the temp file upon destruction. + setenv("PHUSION_PASSENGER_TMP", "utils_test.tmp", 1); + mkdir("utils_test.tmp", S_IRWXU); + { + TempFile t("temp", false); + } + bool dirEmpty = listDir("utils_test.tmp").empty(); + removeDirTree("utils_test.tmp"); + ensure(dirEmpty); + } + + TEST_METHOD(22) { + // The temp file's filename is constructed using the given identifier. + setenv("PHUSION_PASSENGER_TMP", "utils_test.tmp", 1); + mkdir("utils_test.tmp", S_IRWXU); + TempFile t("foobar", false); + vector files(listDir("utils_test.tmp")); + removeDirTree("utils_test.tmp"); + + ensure(files[0].find("foobar") != string::npos); + } + + TEST_METHOD(23) { + // It immediately unlinks the temp file if 'anonymous' is true. + // It creates a temp file inside getPassengerTempDir(). + setenv("PHUSION_PASSENGER_TMP", "utils_test.tmp", 1); + mkdir("utils_test.tmp", S_IRWXU); + TempFile t; + unsigned int size = listDir("utils_test.tmp").size(); + removeDirTree("utils_test.tmp"); + ensure_equals(size, 0u); + } + + /***** Test escapeForXml() *****/ + + TEST_METHOD(25) { + ensure_equals(escapeForXml(""), ""); + ensure_equals(escapeForXml("hello world"), "hello world"); + ensure_equals(escapeForXml("./hello_world/foo.txt"), "./hello_world/foo.txt"); + ensure_equals(escapeForXml("hello 'hello world' + end + end + }) + end + + File.open(restart_file, 'w').close + get('/bar').should == "hello world" + + File.open(controller, 'w') do |f| + f.write(%Q{ + class BarController < ApplicationController + def index + render :text => 'oh hai' + end + end + }) + end + + now = Time.now + File.open(restart_file, 'w').close + File.utime(now - 10, now - 10, restart_file) + get('/bar').should == "oh hai" + ensure + File.unlink(controller) rescue nil + File.unlink(restart_file) rescue nil + end + end + + describe "PassengerUseGlobalQueue" do + after :each do + # Restart Apache to reset the application pool's state. + @apache2.start + end + + it "is off by default" do + @server = "http://mycook.passenger.test:#{@apache2.port}" + + # Spawn the application. + get('/') + + threads = [] + # Reserve all application pool slots. + 3.times do |i| + thread = Thread.new do + File.unlink("#{@stub.app_root}/#{i}.txt") rescue nil + get("/welcome/sleep_until_exists?name=#{i}.txt") + end + threads << thread + end + + # Wait until all application instances are waiting + # for the quit file. + while !File.exist?("#{@stub.app_root}/waiting_0.txt") || + !File.exist?("#{@stub.app_root}/waiting_1.txt") || + !File.exist?("#{@stub.app_root}/waiting_2.txt") + sleep 0.1 + end + + # While all slots are reserved, make two more requests. + first_request_done = false + second_request_done = false + thread = Thread.new do + get("/") + first_request_done = true + end + threads << thread + thread = Thread.new do + get("/") + second_request_done = true + end + threads << thread + + # These requests should both block. + sleep 0.5 + first_request_done.should be_false + second_request_done.should be_false + + # One of the requests should still be blocked + # if one application instance frees up. + File.open("#{@stub.app_root}/2.txt", 'w') + begin + Timeout.timeout(5) do + while !first_request_done && !second_request_done + sleep 0.1 + end + end + rescue Timeout::Error + end + (first_request_done || second_request_done).should be_true + + File.open("#{@stub.app_root}/0.txt", 'w') + File.open("#{@stub.app_root}/1.txt", 'w') + File.open("#{@stub.app_root}/2.txt", 'w') + threads.each do |thread| + thread.join + end + end + + it "works and is per-virtual host" do + @server = "http://passenger.test:#{@apache2.port}" + + # Spawn the application. + get('/') + + threads = [] + # Reserve all application pool slots. + 3.times do |i| + thread = Thread.new do + File.unlink("#{@stub2.app_root}/#{i}.txt") rescue nil + get("/foo/sleep_until_exists?name=#{i}.txt") + end + threads << thread + end + + # Wait until all application instances are waiting + # for the quit file. + while !File.exist?("#{@stub2.app_root}/waiting_0.txt") || + !File.exist?("#{@stub2.app_root}/waiting_1.txt") || + !File.exist?("#{@stub2.app_root}/waiting_2.txt") + sleep 0.1 + end + + # While all slots are reserved, make two more requests. + first_request_done = false + second_request_done = false + thread = Thread.new do + get("/") + first_request_done = true + end + threads << thread + thread = Thread.new do + get("/") + second_request_done = true + end + threads << thread + + # These requests should both block. + sleep 0.5 + first_request_done.should be_false + second_request_done.should be_false + + # Both requests should be processed if one application instance frees up. + File.open("#{@stub2.app_root}/2.txt", 'w') + begin + Timeout.timeout(5) do + while !first_request_done || !second_request_done + sleep 0.1 + end + end + rescue Timeout::Error + end + first_request_done.should be_true + second_request_done.should be_true + + File.open("#{@stub2.app_root}/0.txt", 'w') + File.open("#{@stub2.app_root}/1.txt", 'w') + File.open("#{@stub2.app_root}/2.txt", 'w') + threads.each do |thread| + thread.join + end + end + end + + describe "PassengerAppRoot" do + before :all do + @stub3 = setup_rails_stub('mycook', 'tmp.stub3') + doc_root = File.expand_path(@stub3.app_root) + "/sites/some.site/public" + @apache2.set_vhost('passenger.test', doc_root) do |vhost| + vhost << "PassengerAppRoot #{File.expand_path(@stub3.app_root).inspect}" + end + @apache2.start + end + + after :all do + @stub3.destroy + end + + it "supports page caching on non-index URIs" do + @server = "http://passenger.test:#{@apache2.port}" + get('/welcome/cached.html').should =~ %r{This is the cached version of some.site/public/welcome/cached} + end + + it "supports page caching on index URIs" do + @server = "http://passenger.test:#{@apache2.port}" + get('/uploads.html').should =~ %r{This is the cached version of some.site/public/uploads} + end + + it "works as a rails application" do + @server = "http://passenger.test:#{@apache2.port}" + result = get('/welcome/parameters_test?hello=world&recipe[name]=Green+Bananas') + result.should =~ %r{world} + result.should =~ %r{} + result.should =~ %r{Green Bananas} + end end + + #################################### end describe "error handling" do @@ -385,7 +629,7 @@ def application(env, start_response): FileUtils.rm_rf('tmp.webdir') FileUtils.mkdir_p('tmp.webdir') @webdir = File.expand_path('tmp.webdir') - @apache2.add_vhost('passenger.test', @webdir) do |vhost| + @apache2.set_vhost('passenger.test', @webdir) do |vhost| vhost << "RailsBaseURI /app-with-nonexistant-rails-version/public" vhost << "RailsBaseURI /app-that-crashes-during-startup/public" vhost << "RailsBaseURI /app-with-crashing-vendor-rails/public" @@ -435,7 +679,7 @@ def application(env, start_response): describe "Rack application running in root URI" do before :all do @stub = setup_stub('rack') - @apache2.add_vhost('passenger.test', File.expand_path(@stub.app_root) + "/public") + @apache2.set_vhost('passenger.test', File.expand_path(@stub.app_root) + "/public") @apache2.start @server = "http://passenger.test:#{@apache2.port}" end @@ -452,7 +696,7 @@ def application(env, start_response): FileUtils.rm_rf('tmp.webdir') FileUtils.mkdir_p('tmp.webdir') @stub = setup_stub('rack') - @apache2.add_vhost('passenger.test', File.expand_path('tmp.webdir')) do |vhost| + @apache2.set_vhost('passenger.test', File.expand_path('tmp.webdir')) do |vhost| FileUtils.ln_s(File.expand_path(@stub.app_root) + "/public", 'tmp.webdir/rack') vhost << "RackBaseURI /rack" end @@ -471,7 +715,7 @@ def application(env, start_response): describe "WSGI application running in root URI" do before :all do @stub = setup_stub('wsgi') - @apache2.add_vhost('passenger.test', File.expand_path(@stub.app_root) + "/public") + @apache2.set_vhost('passenger.test', File.expand_path(@stub.app_root) + "/public") @apache2.start @server = "http://passenger.test:#{@apache2.port}" end diff --git a/test/oxt/backtrace_test.cpp b/test/oxt/backtrace_test.cpp new file mode 100644 index 00000000..11906c8c --- /dev/null +++ b/test/oxt/backtrace_test.cpp @@ -0,0 +1,128 @@ +#include "tut.h" +#include +#include +#include + +using namespace oxt; +using namespace std; + +namespace tut { + struct backtrace_test { + }; + + DEFINE_TEST_GROUP(backtrace_test); + + TEST_METHOD(1) { + // Test TRACE_POINT() and tracable_exception. + struct { + void foo() { + TRACE_POINT(); + bar(); + } + + void bar() { + TRACE_POINT(); + baz(); + } + + void baz() { + TRACE_POINT(); + throw tracable_exception(); + } + } object; + + try { + object.foo(); + fail("tracable_exception expected."); + } catch (const tracable_exception &e) { + ensure("Backtrace contains foo()", + e.backtrace().find("foo()") != string::npos); + ensure("Backtrace contains bar()", + e.backtrace().find("bar()") != string::npos); + ensure("Backtrace contains baz()", + e.backtrace().find("baz()") != string::npos); + } + } + + struct QuitEvent { + bool is_done; + boost::mutex mutex; + boost::condition_variable cond; + + QuitEvent() { + is_done = false; + } + + void wait() { + boost::unique_lock l(mutex); + while (!is_done) { + cond.wait(l); + } + } + + void done() { + is_done = true; + cond.notify_all(); + } + }; + + struct FooCaller { + QuitEvent *quit_event; + + static void foo(QuitEvent *quit_event) { + TRACE_POINT(); + quit_event->wait(); + } + + void operator()() { + foo(quit_event); + } + }; + + struct BarCaller { + QuitEvent *quit_event; + + static void bar(QuitEvent *quit_event) { + TRACE_POINT(); + quit_event->wait(); + } + + void operator()() { + bar(quit_event); + } + }; + + TEST_METHOD(2) { + // Test whether oxt::thread's backtrace support works. + FooCaller foo; + QuitEvent foo_quit; + foo.quit_event = &foo_quit; + + BarCaller bar; + QuitEvent bar_quit; + bar.quit_event = &bar_quit; + + oxt::thread foo_thread(foo); + oxt::thread bar_thread(bar); + usleep(20000); + + ensure("Foo thread's backtrace contains foo()", + foo_thread.backtrace().find("foo") != string::npos); + ensure("Foo thread's backtrace doesn't contain bar()", + foo_thread.backtrace().find("bar") == string::npos); + ensure("Bar thread's backtrace contains bar()", + bar_thread.backtrace().find("bar") != string::npos); + ensure("Bar thread's backtrace doesn't contain foo()", + bar_thread.backtrace().find("foo") == string::npos); + + string all_backtraces(oxt::thread::all_backtraces()); + ensure(all_backtraces.find("foo") != string::npos); + ensure(all_backtraces.find("bar") != string::npos); + + foo_quit.done(); + bar_quit.done(); + foo_thread.join(); + bar_thread.join(); + } +} + diff --git a/test/oxt/oxt_test_main.cpp b/test/oxt/oxt_test_main.cpp new file mode 100644 index 00000000..a038f757 --- /dev/null +++ b/test/oxt/oxt_test_main.cpp @@ -0,0 +1,25 @@ +#include "tut.h" +#include "tut_reporter.h" +#include +#include +#include + +namespace tut { + test_runner_singleton runner; +} + +int main() { + tut::reporter reporter; + tut::runner.get().set_callback(&reporter); + signal(SIGPIPE, SIG_IGN); + setenv("RAILS_ENV", "production", 1); + setenv("TESTING_PASSENGER", "1", 1); + oxt::setup_syscall_interruption_support(); + try { + tut::runner.get().run_tests(); + } catch (const std::exception &ex) { + std::cerr << "Exception raised: " << ex.what() << std::endl; + return 1; + } + return 0; +} diff --git a/test/oxt/syscall_interruption_test.cpp b/test/oxt/syscall_interruption_test.cpp new file mode 100644 index 00000000..ceb846c8 --- /dev/null +++ b/test/oxt/syscall_interruption_test.cpp @@ -0,0 +1,50 @@ +#include "tut.h" +#include +#include +#include +#include +#include + +using namespace oxt; +using namespace std; + +namespace tut { + struct SyscallInterruptionTest { + SyscallInterruptionTest() { + setup_syscall_interruption_support(); + } + + ~SyscallInterruptionTest() { + struct sigaction action; + + action.sa_handler = SIG_DFL; + action.sa_flags = 0; + sigemptyset(&action.sa_mask); + sigaction(INTERRUPTION_SIGNAL, &action, NULL); + } + }; + + DEFINE_TEST_GROUP(SyscallInterruptionTest); + + struct SleepFunction { + void operator()() { + syscalls::usleep(6000000); + } + }; + + TEST_METHOD(1) { + // System call interruption works. + SleepFunction s; + oxt::thread thr(s); + usleep(20000); + + time_t begin, end, time_spent_in_thread; + begin = time(NULL); + thr.interrupt_and_join(); + end = time(NULL); + time_spent_in_thread = end - begin; + + ensure(time_spent_in_thread <= 2); + } +} + diff --git a/test/ruby/abstract_request_handler_spec.rb b/test/ruby/abstract_request_handler_spec.rb new file mode 100644 index 00000000..69f0675f --- /dev/null +++ b/test/ruby/abstract_request_handler_spec.rb @@ -0,0 +1,83 @@ +require 'support/config' +require 'support/test_helper' +require 'phusion_passenger/abstract_request_handler' + +require 'fileutils' + +include PhusionPassenger + +describe AbstractRequestHandler do + before :each do + ENV['PHUSION_PASSENGER_TMP'] = "abstract_request_handler_spec.tmp" + @owner_pipe = IO.pipe + @request_handler = AbstractRequestHandler.new(@owner_pipe[1]) + def @request_handler.process_request(*args) + # Do nothing. + end + end + + after :each do + @request_handler.cleanup + @owner_pipe[0].close rescue nil + ENV.delete('PHUSION_PASSENGER_TMP') + FileUtils.rm_rf("abstract_request_handler_spec.tmp") + end + + def prepare + # Do nothing. To be overrided by sub describe blocks. + end + + it "exits if the owner pipe is closed" do + @request_handler.start_main_loop_thread + @owner_pipe[0].close + begin + Timeout.timeout(5) do + wait_until do + !@request_handler.main_loop_running? + end + end + rescue Timeout::Error + violated + end + end + + it "ignores new connections that don't send any data" do + def @request_handler.accept_connection + return nil + end + @request_handler.start_main_loop_thread + wait_until do + @request_handler.iterations != 0 + end + @request_handler.processed_requests.should == 0 + end + + it "creates a socket file in the Phusion Passenger temp folder, unless when using TCP sockets" do + if @request_handler.socket_type == "unix" + Dir["abstract_request_handler_spec.tmp/*"].should_not be_empty + end + end + + it "accepts pings" do + @request_handler.start_main_loop_thread + if @request_handler.socket_type == "unix" + client = UNIXSocket.new(@request_handler.socket_name) + else + addr, port = @request_handler.socket_name.split(/:/) + client = TCPSocket.new(addr, port.to_i) + end + begin + channel = MessageChannel.new(client) + channel.write_scalar("REQUEST_METHOD\0ping\0") + client.read.should == "pong" + ensure + client.close + end + end + + def wait_until + while !yield + sleep 0.01 + end + end +end diff --git a/test/ruby/abstract_server_collection_spec.rb b/test/ruby/abstract_server_collection_spec.rb new file mode 100644 index 00000000..87fbe309 --- /dev/null +++ b/test/ruby/abstract_server_collection_spec.rb @@ -0,0 +1,246 @@ +require 'support/config' +require 'support/test_helper' +require 'phusion_passenger/abstract_server' +require 'phusion_passenger/abstract_server_collection' + +include PhusionPassenger + +describe AbstractServerCollection do + before :each do + @collection = AbstractServerCollection.new + end + + after :each do + @collection.cleanup + end + + specify "#lookup_or_add adds the server returned by its block" do + @collection.synchronize do + @collection.lookup_or_add('foo') do + AbstractServer.new + end + @collection.should have_key('foo') + end + end + + specify "#lookup_or_add does not execute the block if the key exists" do + @collection.synchronize do + @collection.lookup_or_add('foo') do + AbstractServer.new + end + @collection.lookup_or_add('foo') do + violated + end + end + end + + specify "#lookup_or_add returns the found server" do + @collection.synchronize do + server = AbstractServer.new + @collection.lookup_or_add('foo') { server } + result = @collection.lookup_or_add('foo') { AbstractServer.new } + result.should == server + end + end + + specify "#lookup_or_add returns the value of the block if server is not already in the collection" do + @collection.synchronize do + server = AbstractServer.new + result = @collection.lookup_or_add('foo') do + server + end + result.should == server + end + end + + specify "#delete deletes the server with the given key" do + @collection.synchronize do + @collection.lookup_or_add('foo') do + AbstractServer.new + end + @collection.delete('foo') + @collection.should_not have_key('foo') + end + end + + specify "#delete stop the server if it's started" do + @collection.synchronize do + server = AbstractServer.new + @collection.lookup_or_add('foo') do + server.start + server + end + @collection.delete('foo') + server.should_not be_started + end + end + + specify "#clear deletes everything" do + @collection.synchronize do + @collection.lookup_or_add('foo') do + AbstractServer.new + end + @collection.lookup_or_add('bar') do + AbstractServer.new + end + @collection.clear + @collection.should_not have_key('foo') + @collection.should_not have_key('bar') + end + end + + specify "#cleanup deletes everything" do + @collection.synchronize do + @collection.lookup_or_add('foo') do + AbstractServer.new + end + @collection.lookup_or_add('bar') do + AbstractServer.new + end + end + @collection.cleanup + @collection.synchronize do + @collection.should_not have_key('foo') + @collection.should_not have_key('bar') + end + end + + specify "#cleanup stops all servers" do + servers = [] + 3.times do + server = AbstractServer.new + server.start + servers << server + end + @collection.synchronize do + @collection.lookup_or_add('foo') { servers[0] } + @collection.lookup_or_add('bar') { servers[1] } + @collection.lookup_or_add('baz') { servers[2] } + end + @collection.cleanup + servers.each do |server| + server.should_not be_started + end + end + + specify "idle servers are cleaned up periodically" do + foo = AbstractServer.new + foo.max_idle_time = 0.05 + bar = AbstractServer.new + bar.max_idle_time = 2 + + @collection.synchronize do + @collection.lookup_or_add('foo') { foo } + @collection.lookup_or_add('bar') { bar } + end + sleep 0.3 + @collection.synchronize do + @collection.should_not have_key('foo') + @collection.should have_key('bar') + end + end + + specify "servers with max_idle_time of 0 are never cleaned up" do + @collection.synchronize do + @collection.lookup_or_add('foo') { AbstractServer.new } + end + original_cleaning_time = @collection.next_cleaning_time + @collection.check_idle_servers! + + # Wait until the cleaner thread has run. + while original_cleaning_time == @collection.next_cleaning_time + sleep 0.01 + end + + @collection.synchronize do + @collection.should have_key('foo') + end + end + + specify "upon adding a new server to an empty collection, the next cleaning will " << + "be scheduled at that server's next cleaning time" do + server = AbstractServer.new + server.max_idle_time = 10 + @collection.synchronize do + @collection.lookup_or_add('foo') { server } + end + @collection.next_cleaning_time.should == server.next_cleaning_time + end + + specify "upon adding a new server to a nonempty collection, and that server's next cleaning " << + "time is not the smallest of all servers' cleaning times, then the next cleaning schedule " << + "will not change" do + server1 = AbstractServer.new + server1.max_idle_time = 10 + @collection.synchronize do + @collection.lookup_or_add('foo') { server1 } + end + + server2 = AbstractServer.new + server2.max_idle_time = 11 + @collection.synchronize do + @collection.lookup_or_add('bar') { server2 } + end + + @collection.next_cleaning_time.should == server1.next_cleaning_time + end + + specify "upon deleting server from a nonempty collection, and the deleted server's next cleaning " << + "time IS the smallest of all servers' cleaning times, then the next cleaning schedule " << + "will be changed to the smallest cleaning time of all servers" do + server1 = AbstractServer.new + server1.max_idle_time = 10 + @collection.synchronize do + @collection.lookup_or_add('foo') { server1 } + end + + server2 = AbstractServer.new + server2.max_idle_time = 11 + @collection.synchronize do + @collection.lookup_or_add('bar') { server2 } + end + + @collection.synchronize do + @collection.delete('foo') + end + + @collection.next_cleaning_time.should == server2.next_cleaning_time + end + + specify "upon deleting server from a nonempty collection, and the deleted server's next cleaning " << + "time IS NOT the smallest of all servers' cleaning times, then the next cleaning schedule " << + "will not change" do + server1 = AbstractServer.new + server1.max_idle_time = 10 + @collection.synchronize do + @collection.lookup_or_add('foo') { server1 } + end + + server2 = AbstractServer.new + server2.max_idle_time = 11 + @collection.synchronize do + @collection.lookup_or_add('bar') { server2 } + end + + @collection.synchronize do + @collection.delete('bar') + end + + @collection.next_cleaning_time.should == server1.next_cleaning_time + end + + specify "bug check" do + block = lambda do + @collection.synchronize do + @collection.clear + @collection.lookup_or_add('foo') do + s = AbstractServer.new + s.max_idle_time = 0.05 + s + end + @collection.lookup_or_add('bar') { AbstractServer.new } + end + end + block.should_not raise_error + end +end diff --git a/test/ruby/application_spec.rb b/test/ruby/application_spec.rb index e3d3ace8..ff1ebb91 100644 --- a/test/ruby/application_spec.rb +++ b/test/ruby/application_spec.rb @@ -1,7 +1,7 @@ require 'support/config' require 'support/test_helper' -require 'passenger/application' -include Passenger +require 'phusion_passenger/application' +include PhusionPassenger describe Application do include TestHelper @@ -38,6 +38,6 @@ content.sub(/^RAILS_GEM_VERSION = .*$/, "RAILS_GEM_VERSION = '1.9.1972'") end detector = lambda { Application.detect_framework_version(@stub.app_root) } - detector.should raise_error(::Passenger::VersionNotFound) + detector.should raise_error(::PhusionPassenger::VersionNotFound) end end diff --git a/test/ruby/message_channel_spec.rb b/test/ruby/message_channel_spec.rb index 671fdfca..8fa0bb9c 100644 --- a/test/ruby/message_channel_spec.rb +++ b/test/ruby/message_channel_spec.rb @@ -1,7 +1,7 @@ require 'socket' require 'support/config' -require 'passenger/message_channel' -include Passenger +require 'phusion_passenger/message_channel' +include PhusionPassenger describe MessageChannel do describe "scenarios with a single channel" do diff --git a/test/ruby/rack/application_spawner_spec.rb b/test/ruby/rack/application_spawner_spec.rb index 63d5dd11..ead51e65 100644 --- a/test/ruby/rack/application_spawner_spec.rb +++ b/test/ruby/rack/application_spawner_spec.rb @@ -1,10 +1,8 @@ require 'support/config' require 'support/test_helper' -require 'passenger/rack/application_spawner' +require 'phusion_passenger/rack/application_spawner' -include Passenger - -describe Passenger::Rack::ApplicationSpawner do +describe PhusionPassenger::Rack::ApplicationSpawner do include TestHelper before :each do @@ -37,7 +35,7 @@ end if Process.euid == 0 def spawn(*args) - Passenger::Rack::ApplicationSpawner.spawn_application(*args) + PhusionPassenger::Rack::ApplicationSpawner.spawn_application(*args) end end diff --git a/test/ruby/rails/application_spawner_spec.rb b/test/ruby/rails/application_spawner_spec.rb index 9766133c..d82045b4 100644 --- a/test/ruby/rails/application_spawner_spec.rb +++ b/test/ruby/rails/application_spawner_spec.rb @@ -1,14 +1,14 @@ require 'support/config' require 'support/test_helper' -require 'passenger/railz/application_spawner' +require 'phusion_passenger/railz/application_spawner' require 'ruby/rails/minimal_spawner_spec' require 'ruby/spawn_server_spec' require 'ruby/rails/spawner_privilege_lowering_spec' require 'ruby/rails/spawner_error_handling_spec' -include Passenger -include Passenger::Railz +include PhusionPassenger +include PhusionPassenger::Railz describe ApplicationSpawner do include TestHelper @@ -35,10 +35,35 @@ def spawn_arbitrary_application describe ApplicationSpawner do include TestHelper - describe "regular spawning" do + describe "smart spawning" do it_should_behave_like "a minimal spawner" it_should_behave_like "handling errors in application initialization" + it "calls the starting_worker_process event, with forked=true, after a new worker process has been forked off" do + use_rails_stub('foobar') do |stub| + File.append(stub.environment_rb, %q{ + PhusionPassenger.on_event(:starting_worker_process) do |forked| + File.append("result.txt", "forked = #{forked}\n") + end + File.append("result.txt", "end of environment.rb\n"); + }) + + spawner = ApplicationSpawner.new(stub.app_root) + spawner.start + begin + spawner.spawn_application.close + spawner.spawn_application.close + ensure + spawner.stop + end + + contents = File.read("#{stub.app_root}/result.txt") + contents.should == "end of environment.rb\n" + + "forked = true\n" + + "forked = true\n" + end + end + def spawn_stub_application(stub) @spawner = ApplicationSpawner.new(stub.app_root) begin @@ -53,7 +78,25 @@ def spawn_stub_application(stub) describe "conservative spawning" do it_should_behave_like "a minimal spawner" it_should_behave_like "handling errors in application initialization" - + + it "calls the starting_worker_process event, with forked=true, after environment.rb has been loaded" do + use_rails_stub('foobar') do |stub| + File.append(stub.environment_rb, %q{ + PhusionPassenger.on_event(:starting_worker_process) do |forked| + File.append("result.txt", "forked = #{forked}\n") + end + File.append("result.txt", "end of environment.rb\n"); + }) + spawn_stub_application(stub).close + spawn_stub_application(stub).close + contents = File.read("#{stub.app_root}/result.txt") + contents.should == "end of environment.rb\n" + + "forked = false\n" + + "end of environment.rb\n" + + "forked = false\n" + end + end + def spawn_stub_application(stub) @spawner = ApplicationSpawner.new(stub.app_root) return @spawner.spawn_application! @@ -70,12 +113,10 @@ def spawn_stub_application(stub) def spawn_stub_application(options = {}) options = { - :lower_privilege => true, - :lowest_user => CONFIG['lowest_user'] + "lower_privilege" => true, + "lowest_user" => CONFIG['lowest_user'] }.merge(options) - @spawner = ApplicationSpawner.new(@stub.app_root, - options[:lower_privilege], - options[:lowest_user]) + @spawner = ApplicationSpawner.new(@stub.app_root, options) @spawner.start begin app = @spawner.spawn_application @@ -92,12 +133,10 @@ def spawn_stub_application(options = {}) def spawn_stub_application(options = {}) options = { - :lower_privilege => true, - :lowest_user => CONFIG['lowest_user'] + "lower_privilege" => true, + "lowest_user" => CONFIG['lowest_user'] }.merge(options) - @spawner = ApplicationSpawner.new(@stub.app_root, - options[:lower_privilege], - options[:lowest_user]) + @spawner = ApplicationSpawner.new(@stub.app_root, options) begin app = @spawner.spawn_application! yield app diff --git a/test/ruby/rails/framework_spawner_spec.rb b/test/ruby/rails/framework_spawner_spec.rb index b5222c58..add41762 100644 --- a/test/ruby/rails/framework_spawner_spec.rb +++ b/test/ruby/rails/framework_spawner_spec.rb @@ -1,13 +1,13 @@ require 'support/config' require 'support/test_helper' -require 'passenger/railz/framework_spawner' +require 'phusion_passenger/railz/framework_spawner' require 'ruby/rails/minimal_spawner_spec' require 'ruby/spawn_server_spec' require 'ruby/rails/spawner_privilege_lowering_spec' require 'ruby/rails/spawner_error_handling_spec' -include Passenger -include Passenger::Railz +include PhusionPassenger +include PhusionPassenger::Railz # TODO: test whether FrameworkSpawner restarts ApplicationSpawner if it crashed @@ -112,17 +112,15 @@ def load_nonexistant_framework def spawn_stub_application(options = {}) options = { - :lower_privilege => true, - :lowest_user => CONFIG['lowest_user'] + "lower_privilege" => true, + "lowest_user" => CONFIG['lowest_user'] }.merge(options) @stub.use_vendor_rails('minimal') @spawner = FrameworkSpawner.new(:vendor => "#{@stub.app_root}/vendor/rails") @spawner.start begin - app = @spawner.spawn_application(@stub.app_root, - options[:lower_privilege], - options[:lowest_user]) + app = @spawner.spawn_application(@stub.app_root, options) yield app ensure app.close if app diff --git a/test/ruby/rails/minimal_spawner_spec.rb b/test/ruby/rails/minimal_spawner_spec.rb index 3675bd6b..b8e89f9f 100644 --- a/test/ruby/rails/minimal_spawner_spec.rb +++ b/test/ruby/rails/minimal_spawner_spec.rb @@ -35,6 +35,35 @@ end end + it "does not conflict with models in the application that are named 'Passenger'" do + use_rails_stub('foobar') do |stub| + if !File.directory?("#{stub.app_root}/app/models") + Dir.mkdir("#{stub.app_root}/app/models") + end + File.open("#{stub.app_root}/app/models/passenger.rb", 'w') do |f| + f.write(%q{ + class Passenger + def name + return "Gourry Gabriev" + end + end + }) + end + File.append(stub.environment_rb, %q{ + # We explicitly call 'require' here because we might be + # using a stub Rails framework (that doesn't support automatic + # loading of model source files). + require 'app/models/passenger' + File.open('passenger.txt', 'w') do |f| + f.write(Passenger.new.name) + end + }) + spawn_stub_application(stub).close + passenger_name = File.read("#{stub.app_root}/passenger.txt") + passenger_name.should == 'Gourry Gabriev' + end + end + it "loads application_controller.rb instead of application.rb, if the former exists" do use_rails_stub('foobar') do |stub| File.rename("#{stub.app_root}/app/controllers/application.rb", diff --git a/test/ruby/rails/spawner_error_handling_spec.rb b/test/ruby/rails/spawner_error_handling_spec.rb index 944bbb2e..746e1c0b 100644 --- a/test/ruby/rails/spawner_error_handling_spec.rb +++ b/test/ruby/rails/spawner_error_handling_spec.rb @@ -39,7 +39,7 @@ class MyError < StandardError spawn_stub_application(@stub) violated "Spawning the application should have raised an InitializationError." rescue AppInitError => e - e.child_exception.should be_nil + e.child_exception.class.should == SystemExit end end end diff --git a/test/ruby/rails/spawner_privilege_lowering_spec.rb b/test/ruby/rails/spawner_privilege_lowering_spec.rb index 7cbfb7ca..d10c2595 100644 --- a/test/ruby/rails/spawner_privilege_lowering_spec.rb +++ b/test/ruby/rails/spawner_privilege_lowering_spec.rb @@ -65,20 +65,20 @@ it "doesn't switch user if environment.rb is owned by a nonexistant user, and 'lowest_user' doesn't exist either" do File.chown(CONFIG['nonexistant_uid'], nil, @environment_rb) - spawn_stub_application(:lowest_user => CONFIG['nonexistant_user']) do |app| + spawn_stub_application("lowest_user" => CONFIG['nonexistant_user']) do |app| read_dumped_info[:username].should == my_username end end it "doesn't switch user if 'lower_privilege' is set to false" do File.chown(uid_for('normal_user_2'), nil, @environment_rb) - spawn_stub_application(:lower_privilege => false) do |app| + spawn_stub_application("lower_privilege" => false) do |app| read_dumped_info[:username].should == my_username end end it "sets $HOME to the user's home directory, after privilege lowering" do - spawn_stub_application(:lowest_user => CONFIG['normal_user_1']) do |app| + spawn_stub_application("lowest_user" => CONFIG['normal_user_1']) do |app| read_dumped_info[:home].should == Etc.getpwnam(CONFIG['normal_user_1']).dir end end diff --git a/test/ruby/spawn_manager_spec.rb b/test/ruby/spawn_manager_spec.rb index fb61d0c6..34e69cca 100644 --- a/test/ruby/spawn_manager_spec.rb +++ b/test/ruby/spawn_manager_spec.rb @@ -1,13 +1,13 @@ require 'support/config' require 'support/test_helper' -require 'passenger/spawn_manager' +require 'phusion_passenger/spawn_manager' require 'ruby/abstract_server_spec' require 'ruby/rails/minimal_spawner_spec' require 'ruby/rails/spawner_privilege_lowering_spec' require 'ruby/rails/spawner_error_handling_spec' -include Passenger -include Passenger::Utils +include PhusionPassenger +include PhusionPassenger::Utils # TODO: test whether SpawnManager restarts FrameworkSpawner if it crashed @@ -64,7 +64,7 @@ def spawn_arbitrary_application end it "can spawn when the server's not running" do - app = @manager.spawn_application(@stub.app_root) + app = @manager.spawn_application("app_root" => @stub.app_root) app.close end @@ -75,8 +75,7 @@ def spawn_arbitrary_application a.close sleep(1) # Give @manager the chance to start. channel = MessageChannel.new(b) - channel.write("spawn_application", @stub.app_root, "true", - "nobody", "production", "smart", "rails") + channel.write("spawn_application", "app_root", @stub.app_root) channel.read pid, listen_socket = channel.read channel.recv_io.close @@ -98,7 +97,7 @@ def spawn_arbitrary_application content.sub(/^RAILS_GEM_VERSION = .*$/, '') end @stub.dont_use_vendor_rails - @manager.spawn_application(@stub.app_root).close + @manager.spawn_application("app_root" => @stub.app_root).close end it "properly reloads applications that do not specify a Rails version" do @@ -108,7 +107,9 @@ def spawn_arbitrary_application @stub.dont_use_vendor_rails @manager.reload(@stub.app_root) spawners = @manager.instance_eval { @spawners } - spawners.should be_empty + spawners.synchronize do + spawners.should be_empty + end end end @@ -118,8 +119,9 @@ def spawn_arbitrary_application it "can spawn a Rack application" do use_stub('rack') do |stub| @manager = SpawnManager.new - app = @manager.spawn_application(stub.app_root, true, - "nobody", "production", "smart", "rack") + app = @manager.spawn_application( + "app_root" => stub.app_root, + "app_type" => "rack") app.close end end @@ -136,6 +138,14 @@ def spawn_arbitrary_application it_should_behave_like "a minimal spawner" end + describe "smart-lv2 spawning" do + before :each do + @spawn_method = "smart-lv2" + end + + it_should_behave_like "a minimal spawner" + end + describe "conservative spawning" do before :each do @spawn_method = "conservative" @@ -150,8 +160,9 @@ def spawn_arbitrary_application def spawn_stub_application(stub) spawner = SpawnManager.new begin - return spawner.spawn_application(stub.app_root, true, - "nobody", "production", @spawn_method) + return spawner.spawn_application( + "app_root" => stub.app_root, + "spawn_method" => @spawn_method) ensure spawner.cleanup end diff --git a/test/ruby/utils_spec.rb b/test/ruby/utils_spec.rb index afa02132..b7759896 100644 --- a/test/ruby/utils_spec.rb +++ b/test/ruby/utils_spec.rb @@ -1,9 +1,9 @@ require 'support/config' require 'tempfile' -require 'passenger/utils' +require 'phusion_passenger/utils' -include Passenger +include PhusionPassenger describe Utils do include Utils @@ -30,4 +30,38 @@ File.unlink(filename) rescue nil end end + + describe "#passenger_tmpdir" do + before :each do + ENV.delete('PHUSION_PASSENGER_TMP') + end + + after :each do + ENV.delete('PHUSION_PASSENGER_TMP') + end + + it "returns Dir.tmpdir if ENV['PHUSION_PASSENGER_TMP'] is nil" do + passenger_tmpdir(false).should == Dir.tmpdir + end + + it "returns Dir.tmpdir if ENV['PHUSION_PASSENGER_TMP'] is an empty string" do + ENV['PHUSION_PASSENGER_TMP'] = '' + passenger_tmpdir(false).should == Dir.tmpdir + end + + it "returns ENV['PHUSION_PASSENGER_TMP'] if it's set" do + ENV['PHUSION_PASSENGER_TMP'] = '/foo' + passenger_tmpdir(false).should == '/foo' + end + + it "creates the directory if it doesn't exist, if the 'create' argument is true" do + ENV['PHUSION_PASSENGER_TMP'] = 'utils_spec.tmp' + passenger_tmpdir + begin + File.directory?('utils_spec.tmp').should be_true + ensure + Dir.rmdir('utils_spec.tmp') rescue nil + end + end + end end diff --git a/test/ruby/wsgi/application_spawner_spec.rb b/test/ruby/wsgi/application_spawner_spec.rb new file mode 100644 index 00000000..ba62075d --- /dev/null +++ b/test/ruby/wsgi/application_spawner_spec.rb @@ -0,0 +1,47 @@ +require 'support/config' +require 'support/test_helper' +require 'phusion_passenger/wsgi/application_spawner' +require 'phusion_passenger/utils' +require 'fileutils' +require 'tempfile' + +describe PhusionPassenger::WSGI::ApplicationSpawner do + include TestHelper + include PhusionPassenger::Utils + + before :each do + ENV['PHUSION_PASSENGER_TMP'] = "#{Dir.tmpdir}/wsgi_test.tmp" + @stub = setup_stub('wsgi') + File.unlink("#{@stub.app_root}/passenger_wsgi.pyc") rescue nil + end + + after :each do + @stub.destroy + ENV.delete('PHUSION_PASSENGER_TMP') + FileUtils.rm_rf("wsgi_test.tmp") + end + + it "can spawn our stub application" do + spawn(@stub.app_root).close + end + + it "creates a socket in Phusion Passenger's temp directory" do + begin + app = spawn(@stub.app_root) + Dir["#{passenger_tmpdir}/passenger_wsgi.*"].should have(1).item + ensure + app.close rescue nil + end + end + + specify "the backend process deletes its socket upon termination" do + spawn(@stub.app_root).close + sleep 0.2 # Give it some time to terminate. + Dir["#{passenger_tmpdir}/passenger_wsgi.*"].should be_empty + end + + def spawn(*args) + PhusionPassenger::WSGI::ApplicationSpawner.spawn_application(*args) + end +end + diff --git a/test/stub/apache2/httpd.conf.erb b/test/stub/apache2/httpd.conf.erb index 13bb1450..1d1fcac4 100644 --- a/test/stub/apache2/httpd.conf.erb +++ b/test/stub/apache2/httpd.conf.erb @@ -29,7 +29,7 @@ Listen 127.0.0.1:<%= @port %> LoadModule passenger_module "<%= @mod_passenger %>" PassengerRoot "<%= @passenger_root %>" -PassengerRuby "<%= RUBY %>" +PassengerRuby "<%= PlatformInfo::RUBY %>" RailsEnv production RackEnv production <% for line in @extra %> @@ -61,7 +61,7 @@ DocumentRoot "<%= @server_root %>" LockFile <%= @server_root %>/httpd.lock PidFile <%= @server_root %>/httpd.pid -ErrorLog <%= @server_root %>/errors.log +ErrorLog <%= @passenger_root %>/test/test.log CustomLog <%= @server_root %>/access.log combined <% if !vhosts.empty? %> @@ -71,6 +71,7 @@ CustomLog <%= @server_root %>/access.log combined > ServerName <%= vhost.domain %> DocumentRoot "<%= vhost.document_root %>" + AllowEncodedSlashes On <% for line in vhost.additional_configs %> <%= line %> <% end %> diff --git a/test/stub/message_channel.rb b/test/stub/message_channel.rb index 2f0daae9..2fadee9c 100644 --- a/test/stub/message_channel.rb +++ b/test/stub/message_channel.rb @@ -1,8 +1,8 @@ #!/usr/bin/env ruby $LOAD_PATH << "#{File.dirname(__FILE__)}/../../lib" -require 'passenger/message_channel' +require 'phusion_passenger/message_channel' -include Passenger +include PhusionPassenger reader = MessageChannel.new(STDIN) writer = MessageChannel.new(STDOUT) writer.write(*(reader.read << "!!")) diff --git a/test/stub/message_channel_2.rb b/test/stub/message_channel_2.rb index 10352b9e..3f664608 100644 --- a/test/stub/message_channel_2.rb +++ b/test/stub/message_channel_2.rb @@ -1,8 +1,8 @@ #!/usr/bin/env ruby $LOAD_PATH << "#{File.dirname(__FILE__)}/../../lib" -require 'passenger/message_channel' +require 'phusion_passenger/message_channel' -include Passenger +include PhusionPassenger reader = MessageChannel.new(STDIN) writer = MessageChannel.new(STDOUT) writer.write_scalar(reader.read_scalar << "!!") diff --git a/test/stub/message_channel_3.rb b/test/stub/message_channel_3.rb index c73c7c0b..5695d2c0 100644 --- a/test/stub/message_channel_3.rb +++ b/test/stub/message_channel_3.rb @@ -1,10 +1,10 @@ #!/usr/bin/env ruby $LOAD_PATH << "#{File.dirname(__FILE__)}/../../lib" $LOAD_PATH << "#{File.dirname(__FILE__)}/../../ext" -require 'passenger/message_channel' -require 'passenger/utils' +require 'phusion_passenger/message_channel' +require 'phusion_passenger/utils' -include Passenger +include PhusionPassenger channel = MessageChannel.new(IO.new(3)) channel.write(*channel.read) channel.write_scalar(channel.read_scalar) diff --git a/test/stub/rails_apps/foobar/app/controllers/foo_controller.rb b/test/stub/rails_apps/foobar/app/controllers/foo_controller.rb index 08e9b2d0..8afdc612 100644 --- a/test/stub/rails_apps/foobar/app/controllers/foo_controller.rb +++ b/test/stub/rails_apps/foobar/app/controllers/foo_controller.rb @@ -10,4 +10,12 @@ def rails_env def backtrace render :text => caller.join("\n") end + + def sleep_until_exists + File.open("#{RAILS_ROOT}/waiting_#{params[:name]}", 'w') + while !File.exist?("#{RAILS_ROOT}/#{params[:name]}") + sleep 0.1 + end + render :nothing => true + end end diff --git a/test/stub/rails_apps/foobar/config/environments/development.rb b/test/stub/rails_apps/foobar/config/environments/development.rb index 09a451f9..d67452f0 100644 --- a/test/stub/rails_apps/foobar/config/environments/development.rb +++ b/test/stub/rails_apps/foobar/config/environments/development.rb @@ -12,7 +12,6 @@ config.action_controller.consider_all_requests_local = true config.action_view.debug_rjs = true config.action_controller.perform_caching = false -config.action_view.cache_template_extensions = false # Don't care if the mailer can't send -config.action_mailer.raise_delivery_errors = false \ No newline at end of file +config.action_mailer.raise_delivery_errors = false diff --git a/test/stub/rails_apps/mycook/app/controllers/welcome_controller.rb b/test/stub/rails_apps/mycook/app/controllers/welcome_controller.rb index eecb801e..4319baf4 100644 --- a/test/stub/rails_apps/mycook/app/controllers/welcome_controller.rb +++ b/test/stub/rails_apps/mycook/app/controllers/welcome_controller.rb @@ -18,7 +18,7 @@ def touch end def in_passenger - render :text => !!defined?(Passenger::SpawnManager) + render :text => !!defined?(IN_PHUSION_PASSENGER) end def rails_env @@ -29,7 +29,27 @@ def backtrace render :text => caller.join("\n") end + def passenger_name + render :text => Passenger.new.name + end + def terminate exit! end + + def show_id + render :text => params[:id] + end + + def request_uri + render :text => request.request_uri + end + + def sleep_until_exists + File.open("#{RAILS_ROOT}/waiting_#{params[:name]}", 'w') + while !File.exist?("#{RAILS_ROOT}/#{params[:name]}") + sleep 0.1 + end + render :nothing => true + end end diff --git a/test/stub/rails_apps/mycook/public/.htaccess b/test/stub/rails_apps/mycook/public/.htaccess index 8ad3bd40..c55b1c47 100644 --- a/test/stub/rails_apps/mycook/public/.htaccess +++ b/test/stub/rails_apps/mycook/public/.htaccess @@ -28,7 +28,7 @@ RewriteEngine On # RewriteBase /myrailsapp RewriteRule ^$ index.html [QSA] -RewriteRule ^([^.]+)$ $1.html [QSA] +#RewriteRule ^([^.]+)$ $1.html [QSA] RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^(.*)$ dispatch.cgi [QSA,L] diff --git a/test/stub/rails_apps/mycook/sites/some.site/public/uploads.html b/test/stub/rails_apps/mycook/sites/some.site/public/uploads.html new file mode 100644 index 00000000..840a0c15 --- /dev/null +++ b/test/stub/rails_apps/mycook/sites/some.site/public/uploads.html @@ -0,0 +1,26 @@ + + + + MyCook™ beta + + + + +
+ This is the cached version of some.site/public/uploads. +
+ + diff --git a/test/stub/rails_apps/mycook/sites/some.site/public/welcome/cached.html b/test/stub/rails_apps/mycook/sites/some.site/public/welcome/cached.html new file mode 100644 index 00000000..ec9ea097 --- /dev/null +++ b/test/stub/rails_apps/mycook/sites/some.site/public/welcome/cached.html @@ -0,0 +1,26 @@ + + + + MyCook™ beta + + + + +
+ This is the cached version of some.site/public/welcome/cached. +
+ + diff --git a/test/stub/railsapp/app/controllers/bar_controller_2.rb b/test/stub/railsapp/app/controllers/bar_controller_2.rb index 2b60ecfb..6cccbb72 100644 --- a/test/stub/railsapp/app/controllers/bar_controller_2.rb +++ b/test/stub/railsapp/app/controllers/bar_controller_2.rb @@ -1,5 +1,5 @@ class BarController < ApplicationController def index - render :text => 'bar 1!' + render :text => 'bar 2!' end end diff --git a/test/stub/railsapp/app/controllers/foo_controller.rb b/test/stub/railsapp/app/controllers/foo_controller.rb index b46a9e0b..94cb12d7 100644 --- a/test/stub/railsapp/app/controllers/foo_controller.rb +++ b/test/stub/railsapp/app/controllers/foo_controller.rb @@ -2,4 +2,8 @@ class FooController < ActionController::Base def new render :text => 'hello world' end + + def pid + render :text => Process.pid.to_s + end end diff --git a/test/stub/spawn_server.rb b/test/stub/spawn_server.rb index 379756b0..1a0110f9 100644 --- a/test/stub/spawn_server.rb +++ b/test/stub/spawn_server.rb @@ -1,12 +1,11 @@ #!/usr/bin/env ruby $LOAD_PATH << "#{File.dirname(__FILE__)}/../../lib" $LOAD_PATH << "#{File.dirname(__FILE__)}/../../ext" -require 'passenger/spawn_manager' +require 'phusion_passenger/spawn_manager' -include Passenger +include PhusionPassenger class SpawnManager - def handle_spawn_application(app_root, lower_privilege, lowest_user, environment, - spawn_method, app_type) + def handle_spawn_application(*options) client.write('ok') client.write(1234, "/tmp/nonexistant.socket", false) client.send_io(STDERR) diff --git a/test/support/apache2_controller.rb b/test/support/apache2_controller.rb index f45f8976..75292f12 100644 --- a/test/support/apache2_controller.rb +++ b/test/support/apache2_controller.rb @@ -1,9 +1,45 @@ require 'erb' require 'fileutils' -require 'passenger/platform_info' +require 'phusion_passenger/platform_info' +# A class for starting, stopping and restarting Apache, and for manipulating +# its configuration file. This is used by the integration tests. +# +# Before a test begins, the test instructs Apache2Controller to create an Apache +# configuration folder, which contains an Apache configuration file and other +# configuration resources that Apache needs. The Apache configuration file is +# created from a template (see Apache2Controller::STUB_DIR). +# The test can define configuration customizations. For example, it can tell +# Apache2Controller to add configuration options, virtual host definitions, etc. +# +# After the configuration folder has been created, Apache2Controller will start +# Apache. After Apache has been started, the test will be run. Apache2Controller +# will stop Apache after the test is done. +# +# Apache2Controller ensures that starting, stopping and restarting are not prone +# to race conditions. For example, it ensures that when #start returns, Apache +# really is listening on its server socket instead of still initializing. +# +# == Usage +# +# Suppose that you want to test a hypothetical "AlwaysPrintHelloWorld" +# Apache configuration option. Then you can write the following test: +# +# apache = Apache2Controller.new +# +# # Add a configuration option to the configuration file. +# apache << "AlwaysPrintHelloWorld on" +# +# # Write configuration file and start Apache with that configuration file. +# apache.start +# +# begin +# response_body = http_get("http://localhost:#{apache.port}/some/url") +# response_body.should == "hello world!" +# ensure +# apache.stop +# end class Apache2Controller - include PlatformInfo STUB_DIR = File.expand_path(File.dirname(__FILE__) + "/../stub/apache2") class VHost @@ -41,6 +77,11 @@ def set(options) end end + # Create an Apache configuration folder and start Apache on that + # configuration folder. This method does not return until Apache + # has done initializing. + # + # If Apache is already started, this this method will stop Apache first. def start if running? stop @@ -55,7 +96,7 @@ def start write_config_file FileUtils.cp("#{STUB_DIR}/mime.types", @server_root) - if !system(HTTPD, "-f", "#{@server_root}/httpd.conf", "-k", "start") + if !system(PlatformInfo.httpd, "-f", "#{@server_root}/httpd.conf", "-k", "start") raise "Could not start an Apache server." end @@ -87,11 +128,15 @@ def start def graceful_restart write_config_file - if !system(HTTPD, "-f", "#{@server_root}/httpd.conf", "-k", "graceful") + if !system(PlatformInfo.httpd, "-f", "#{@server_root}/httpd.conf", "-k", "graceful") raise "Cannot restart Apache." end end + # Stop Apache and delete its configuration folder. This method waits + # until Apache is done with its shutdown procedure. + # + # This method does nothing if Apache is already stopped. def stop pid_file = "#{@server_root}/httpd.pid" if File.exist?(pid_file) @@ -132,14 +177,18 @@ def stop end end - def add_vhost(domain, document_root) + # Define a virtual host configuration block for the Apache configuration + # file. + def set_vhost(domain, document_root) vhost = VHost.new(domain, document_root) if block_given? yield vhost end + vhosts.reject! {|host| host.domain == domain} vhosts << vhost end + # Checks whether this Apache instance is running. def running? if File.exist?("#{@server_root}/httpd.pid") pid = File.read("#{@server_root}/httpd.pid").strip @@ -156,6 +205,7 @@ def running? end end + # Defines a configuration snippet to be added to the Apache configuration file. def <<(line) @extra << line end @@ -173,11 +223,11 @@ def write_config_file end def modules_dir - @@modules_dir ||= `#{APXS2} -q LIBEXECDIR`.strip + @@modules_dir ||= `#{PlatformInfo.apxs2} -q LIBEXECDIR`.strip end def builtin_modules - @@builtin_modules ||= `#{HTTPD} -l`.split("\n").grep(/\.c$/).map do |line| + @@builtin_modules ||= `#{PlatformInfo.httpd} -l`.split("\n").grep(/\.c$/).map do |line| line.strip end end diff --git a/test/support/tut.h b/test/support/tut.h index efeaa6c3..7c509982 100644 --- a/test/support/tut.h +++ b/test/support/tut.h @@ -42,6 +42,21 @@ #include #endif +#ifdef __SOLARIS9__ +// Solaris 9 only has putenv, not setenv. +static int setenv(const char *name, const char *value, int override) { + int ret; + char *s = (char*)malloc(strlen(name) + strlen(value) + 2); + s[0] = 0; + strcpy(s, name); + strcpy(s, "="); + strcpy(s, value); + ret = putenv(s); + free(s); + return ret; +} +#endif + #define DEFINE_TEST_GROUP(name) \ using namespace tut; \ typedef test_group factory; \ diff --git a/vendor/README b/vendor/README new file mode 100644 index 00000000..f3416644 --- /dev/null +++ b/vendor/README @@ -0,0 +1,12 @@ +You might be wondering why the Rack library is vendored, and why we don't +just depend on the Rack gem. The reason for this is because there are broken +applications out there that have a hard dependency on rack == 0.4.0 (the +latest version of Rack is 0.9.1 at the time of writing). If Passenger +depends on the Rack gem, then the application will crash with a gem version +conflict error upon executing 'gem "rack", "=0.4.0"'. + +To fix this conflict, we vendor Rack. When we load our vendored Rack library, +it won't be registered as a gem, so no RubyGems version conflict exception +will be raised. + +Rack is backwards-compatible so there shouldn't be any problems. diff --git a/vendor/README_FOR_PACKAGERS b/vendor/README_FOR_PACKAGERS new file mode 100644 index 00000000..232ca536 --- /dev/null +++ b/vendor/README_FOR_PACKAGERS @@ -0,0 +1 @@ +Rack is vendored for a reason, don't try to remove it. See README. diff --git a/vendor/rack-0.9.1/AUTHORS b/vendor/rack-0.9.1/AUTHORS new file mode 100644 index 00000000..dc0203de --- /dev/null +++ b/vendor/rack-0.9.1/AUTHORS @@ -0,0 +1,8 @@ +* Christian Neukirchen +* HTTP authentication: Tim Fletcher +* Cookie sessions, Static handler: Luc Heinrich +* Pool sessions, OpenID authentication: blink +* Rack::Deflater: Christoffer Sawicki +* LiteSpeed handler: Adrian Madrid +* SCGI handler: Jeremy Evans +* Official Logo: Armin Ronacher diff --git a/vendor/rack-0.9.1/COPYING b/vendor/rack-0.9.1/COPYING new file mode 100644 index 00000000..8ed138b9 --- /dev/null +++ b/vendor/rack-0.9.1/COPYING @@ -0,0 +1,18 @@ +Copyright (c) 2007 Christian Neukirchen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/rack-0.9.1/ChangeLog b/vendor/rack-0.9.1/ChangeLog new file mode 100644 index 00000000..059ae933 --- /dev/null +++ b/vendor/rack-0.9.1/ChangeLog @@ -0,0 +1,1423 @@ +Fri Jan 9 17:32:54 2009 +0100 Christian Neukirchen + * Fix directory traversal exploits in Rack::File and Rack::Directory + +Tue Jan 6 12:56:00 2009 +0100 Christian Neukirchen + * Last minute README fixes + +Tue Jan 6 12:46:37 2009 +0100 Christian Neukirchen + * Fix last glitches + +Tue Jan 6 12:44:44 2009 +0100 Christian Neukirchen + * Set release date + +Mon Jan 5 17:44:36 2009 -0800 Jon Crosby + * Store original HTTP method in MethodOverride middleware + +Tue Jan 6 12:30:29 2009 +0100 Christian Neukirchen + * Fix typos in auth/openid + Reported by Robert Adkins + +Mon Jan 5 18:41:15 2009 +0100 Christian Neukirchen + * Rack::File::MIME_TYPES is now Rack::Mime::MIME_TYPES + +Mon Jan 5 18:35:31 2009 +0100 Christian Neukirchen + * Update gemspec + +Mon Jan 5 18:46:08 2009 +0100 Christian Neukirchen + * Revert "Added Rack::Request initialization memoization to reduce repetitive instantiation cost." + Potentially causes problems with inheritance. + + This reverts commits: + 4cf6f6eb0dd8fdb415016a4e2f41d1784146cd7a + 552f7b0718ee8cd79c185cd72413690f0da72402 + eefbed89c4ece749e889132012d0f67cd87926a8 + +Mon Jan 5 18:42:09 2009 +0100 Christian Neukirchen + * Branch 0.9 + +Mon Jan 5 18:16:36 2009 +0100 Christian Neukirchen + * Update thanks + +Mon Jan 5 18:16:24 2009 +0100 Christian Neukirchen + * Update copyright + +Mon Jan 5 18:06:11 2009 +0100 Christian Neukirchen + * Update README + +Mon Jan 5 15:00:15 2009 +0100 Christian Neukirchen + * In URLMap, entries without host name should come first + +Mon Jan 5 14:59:38 2009 +0100 Christian Neukirchen + * Marshall of String changed in 1.9 + +Mon Jan 5 14:59:27 2009 +0100 Christian Neukirchen + * Rewrite Response test to use a well-defined #each + +Mon Jan 5 14:59:06 2009 +0100 Christian Neukirchen + * Array#to_a changed in 1.9 + +Mon Jan 5 14:58:45 2009 +0100 Christian Neukirchen + * Constants are symbols in 1.9 + +Mon Jan 5 13:51:20 2009 +0100 Christian Neukirchen + * Shuffle scopes for 1.9 + +Mon Jan 5 05:41:13 2009 -0400 raggi + * Fix spec_rack_response for 1.9 + +Sun Jan 4 23:19:31 2009 +0900 Michael Fellinger + * Fix webrick handler for ruby 1.9.1 + +Tue Dec 30 22:03:42 2008 +0100 Christian Neukirchen + * Merge commit 'official/master' + +Tue Dec 30 21:48:17 2008 +0100 Christian Neukirchen + * Add trailing slash to the alternative gem server + +Mon Dec 29 22:31:27 2008 -0600 Joshua Peek + * Support X-Http-Method-Override header in MethodOverride middleware + +Tue Dec 30 12:23:26 2008 +0100 Christian Neukirchen + * Don't leak absolute paths in error messages + Reported by Yonghui Luo. + +Mon Dec 29 02:15:25 2008 -0800 Ryan Tomayko + * Implement HeaderHash#merge! and HeaderHash#merge + +Mon Dec 29 00:40:46 2008 -0800 Ryan Tomayko + * Use HeaderHash where header case should be insensitive + + The ConditionalGet, ContentLength, Deflator, and ShowStatus + middleware components were reading/checking headers case + sensitively. + +Thu Dec 11 21:00:27 2008 -0800 Ryan Tomayko + * Non-normalizing HeaderHash with case-insensitive lookups + + This is a backwards incompatible change that removes header name + normalization while attempting to keep most of its benefits. The + header name case is preserved but the Hash has case insensitive + lookup, replace, delete, and include semantics. + +Mon Dec 29 11:49:29 2008 -0600 Joshua Peek + * Don't try to rewind CGI input + +Sun Dec 28 14:08:47 2008 +0100 Christian Neukirchen + * Reformat Rack::Deflater code + +Tue Dec 23 00:23:49 2008 -0800 Ryan Tomayko + * Rack::Deflator respects the no-transform cache control directive + +Thu Dec 25 12:20:50 2008 +0100 Christian Neukirchen + * Update README + +Thu Dec 25 12:09:42 2008 +0100 Christian Neukirchen + * Idiomize code + +Wed Dec 24 19:33:17 2008 -0500 Matt Todd + * Added specification for Rack::Request memoization. + +Wed Dec 24 19:25:20 2008 -0500 Matt Todd + * Updated spec with the new size of the content length based on the new environment variable data included with the Rack::Request instantiation memoization. + +Wed Dec 24 19:24:44 2008 -0500 Matt Todd + * Added Rack::Request initialization memoization to reduce repetitive instantiation cost. + +Tue Dec 23 21:32:38 2008 -0600 Joshua Peek + * Rewind input after parsing request form vars + +Tue Dec 23 21:22:50 2008 -0600 Joshua Peek + * Delegate Lint::InputWrapper#rewind to underlying IO object + +Tue Dec 23 11:52:11 2008 -0800 Ryan Tomayko + * use Set instead of Array for STATUS_WITH_NO_ENTITY_BODY + +Mon Dec 22 22:17:18 2008 -0800 Ryan Tomayko + * Rack::ContentLength tweaks ... + + * Adds a Content-Length header only when the body is of knownable + length (String, Array). + * Does nothing when Transfer-Encoding header is present in + response. + * Uses a Set instead of an Array for status code lookup (linear + search through 102 elements seemed expensive). + +Sat Dec 20 13:36:22 2008 -0800 Dan Kubb + * Fixed Rack::Deflater to handle responses with Last-Modified header + + * There was a bug when performing gzip compression where the + Last-Modified response header was assumed to be a Time object, + and passed directly to Zlib::GzipWriter#mtime, causing an exception + since it is always a String. + + This fix parses the Last-Modified header using Time.httpdate and + returns a Time obejct, which can be safely passed to + Zlib::GzipWriter#mtime. + +Sat Dec 20 13:23:05 2008 -0800 Dan Kubb + * Do not add Content-Encoding for a response without and entity body + +Sat Dec 20 13:17:18 2008 -0800 Dan Kubb + * Updated Rack::Deflater spec helper to allow setting the default status + +Sat Dec 20 13:06:28 2008 -0800 Dan Kubb + * Moved STATUS_WITH_NO_ENTITY_BODY into Rack::Utils + + * Removed duplicate constant from Rack::ContentLength and Rack::Lint + +Sat Dec 20 13:00:58 2008 -0800 Dan Kubb + * Added Accept-Encoding to HTTP Vary header + +Fri Dec 19 15:24:21 2008 +0100 Christian Neukirchen + * Merge commit 'rtomayko/methodoverride' + +Thu Dec 18 19:25:24 2008 -0800 Ryan Tomayko + * Fix MethodOverride error when POST has no _method param + +Wed Dec 17 10:02:15 2008 -0500 macournoyer + * Add autoload for Thin handler + +Tue Dec 16 21:48:21 2008 -0500 macournoyer + * Add Thin handler + +Tue Dec 9 10:34:19 2008 -0600 Joshua Peek + * Add ContentLength middleware + +Mon Dec 1 22:24:23 2008 -0700 kastner + * fixing camping bug. see gist:26011 + +Tue Dec 2 11:28:49 2008 -0600 Joshua Peek + * Correct status code language to follow RFC 2616 + +Wed Nov 19 22:07:38 2008 +0100 Daniel Roethlisberger + * Improve session id security: Make session id size configurable, raise the default size from 32 bits to 128 bits, and refactor to allow for easy monkey patching the actual session id generation. Modified version according to feedback. + +Wed Nov 19 22:23:30 2008 +0100 Daniel Roethlisberger + * Add support for Secure and HttpOnly flags to session cookies. Set HttpOnly flag by default, since normally, there is no need to read a Rack session from JavaScript in the browser. Do not set the Secure flag by default, since that breaks if the application is not served over TLS. + +Fri Oct 17 11:43:25 2008 -0700 Eric Wong + * Avoid slurping or parsing request body on PUT requests + + Uploading a large file via the HTTP PUT method causes + `@env["rack.input"].read' to be called inside the POST method. This + means the entire file is slurped into memory and was needlessly causing + my Sinatra process to eat up 300M+ for some uploads I've been doing. + +Thu Nov 20 14:49:32 2008 -0800 postmodern + * Use the universally supported select event handler for lighttpd. + + * freebsd-kqueue is obviously not supported on Linux. + +Thu Nov 20 00:14:21 2008 -0800 postmodern + * When calling map, create another object of the same class. + + * This allows one to extend Rack::Builder to create specialized Rack + DSLs. + +Fri Nov 28 15:51:48 2008 +0100 Christian Neukirchen + * Silence Net::HTTP warning + +Tue Nov 25 16:33:27 2008 -0800 Phil Hagelberg + * Updated the tests to use net/http since open-uri doesn't stream responses. + + Oh, and now the tests actually pass. + +Tue Nov 25 16:16:39 2008 -0800 Phil Hagelberg + * Allow streaming with the Mongrel Handler. + + Write directly to the socket instead of keeping it in the Mongrel Response body. + Send the status/headers up front. + +Tue Nov 25 15:29:24 2008 -0800 Phil Hagelberg + * Add tests for streaming with Mongrel. + +Sun Oct 19 00:15:49 2008 -0600 Ben Alpert + * Implemented Rack::Head, modified Rack::Lint to ensure responses to HEAD requests have empty bodies + +Sat Oct 11 16:45:41 2008 +0200 Christian Neukirchen + * Fix header emission for WEBrick and Set-Cookie + Found by Michael Fellinger. + This does not fix Set-Cookie2, Warning, or WWW-Authenticate, because + WEBrick has no way to have duplicates for them. + +Wed Oct 1 12:10:40 2008 +0200 Christian Neukirchen + * Test that Rack::Session::Cookie ignores tampered with session cookies + by Christoffer Sawicki + +Tue Sep 30 19:18:35 2008 +0200 Christian Neukirchen + * Add secure cookies + Proposed by necrodome. + +Tue Sep 30 17:25:29 2008 +0900 Michael Fellinger + * Empty is if Content-Length is 0, [''] ain't empty? + +Tue Sep 16 11:50:27 2008 +0200 Christian Neukirchen + * Rewrite Rack::Builder tests to avoid race-conditions + +Sat Sep 13 04:28:51 2008 -0400 Matt Todd + * Added another example demonstrating the Rack::Builder.app method. + +Sat Sep 13 04:21:38 2008 -0400 Matt Todd + * Added spec for application initialization to be performed only once. + +Sat Sep 13 03:47:12 2008 -0400 Matt Todd + * Implemented Rack::Builder.app and added specs. + +Wed Sep 10 18:56:46 2008 +0200 Christian Neukirchen + * Add :secure option for set_cookie + By Brad Hilton. + +Tue Sep 9 11:25:49 2008 +0200 Christian Neukirchen + * ConditionalGet middleware (Last-Modified/Etag) + + Adapted from Michael Klishin's implementation for Merb: + http://github.com/wycats/merb-core/tree/master/lib/merb-core/rack/middleware/conditional_get.rb + + Implemented by Ryan Tomayko. + +Sun Sep 7 12:20:22 2008 -0500 Joshua Peek + * Add MethodOverride middleware to allow browsers to fake PUT and DELETE methods + +Sun Sep 7 20:20:30 2008 +0200 Christian Neukirchen + * Update emongrel and add swiftiplied mongrel + +Sun Sep 7 20:15:26 2008 +0200 Christian Neukirchen + * Update Rack::File + + * Fix trouble with wrong Content-Length if File.size returns 0 + * Use Rack::Mime + * Split _call into methods for easier subclassing + + Based on a patch by Michael Fellinger. + +Sun Sep 7 19:52:15 2008 +0200 Christian Neukirchen + * New version of Rack::Directory + + * Handles symlinks + * Less disk access + * Uses UTF8 + * Human-readable filesize from Bytes to Terabytes + * Uses Rack::File as app by default + * Does a File.expand_path on the + * +root+ argument + * Splits up the _call + * method for easier + * subclassing + * Use new Rack::Mime + + Based on a patch by Michael Fellinger. + +Sun Sep 7 17:51:44 2008 +0200 Christian Neukirchen + * Add Rack::Mime, a module containing a MIME-type list and helpers + Proposed and implemented by Michael Fellinger. + +Fri Sep 5 22:22:16 2008 +0300 Michael S. Klishin + * Make Rack::Lint::InputWrapper delegate size method to underlying IO object. + + See http://snurl.com/3nesq: Lint was breaking file uploads in a Merb app. + + Signed-off-by: Michael S. Klishin + +Sat Aug 30 16:47:50 2008 +0900 Michael Fellinger + * Add Request#ip and corresponding spec + +Thu Aug 28 15:57:14 2008 +0200 Christian Neukirchen + * Make Rack::Lobster set Content-Length + +Thu Aug 28 15:55:19 2008 +0200 Christian Neukirchen + * Make Rack::ShowExceptions set Content-Length + +Thu Aug 28 15:54:21 2008 +0200 Christian Neukirchen + * Make Rack::Response count Content-Length + +Thu Aug 28 15:47:47 2008 +0200 Christian Neukirchen + * Remove empty FastCGI headers nginx likes to pass + +Thu Aug 21 12:26:47 2008 +0200 Christian Neukirchen + * Update to version 0.4 + +Thu Aug 21 13:24:41 2008 +0200 Christian Neukirchen + * Cosmetics + +Thu Aug 21 12:26:36 2008 +0200 Christian Neukirchen + * Fix packaging script + +Thu Aug 21 12:13:57 2008 +0200 Christian Neukirchen + * Update README + +Tue Aug 19 13:15:18 2008 +0200 Christian Neukirchen + * REQUEST_METHOD only must be a valid token + +Sat Aug 9 18:53:04 2008 +0200 Christian Neukirchen + * Improve test documentation + +Sat Aug 9 18:52:33 2008 +0200 Christian Neukirchen + * Don't test OpenID in the default test suite + +Sat Aug 9 18:52:03 2008 +0200 Christian Neukirchen + * Wrangle paths so finally lighttpd should start everything on all platforms correctly + +Sat Aug 9 18:50:33 2008 +0200 Christian Neukirchen + * Don't test openid if not available + +Sat Aug 9 18:49:53 2008 +0200 Christian Neukirchen + * Don't test mongrel if not available + +Sat Aug 9 18:29:44 2008 +0200 Christian Neukirchen + * Silence OpenID warnings + +Sat Aug 9 18:29:15 2008 +0200 Christian Neukirchen + * Make memcache tests start and kill memcached itself + +Thu Aug 7 03:32:31 2008 -0700 Scytrin dai Kinthra + * BUG: Output of date in wrong time format for cookie expiration (fixed) + Altered test output to match correct name of gem needing to be installed for memcache + +Thu Aug 7 03:01:31 2008 -0700 Scytrin dai Kinthra + * Merge commit 'core/master' + +Fri Aug 1 12:24:43 2008 +0200 Christian Neukirchen + * Make Rack::Lint threadsafe + reported by Sunny Hirai + +Thu Jul 24 11:26:17 2008 +0200 Christian Neukirchen + * Merge git://github.com/dkubb/rack + +Thu Jul 24 01:40:18 2008 -0700 Dan Kubb + * Ensure the comparison is case insensitive + +Thu Jul 24 01:12:25 2008 -0700 Dan Kubb + * Updated Rake::Lint to ensure Content-Length header is present for non-chunked responses + +Sat Jul 12 12:47:35 2008 +0200 Julik + * Make Lint show proper errors for headers + +Wed Jul 9 15:18:35 2008 +0200 Clive Crous + * Fix digest paramater scanning. + Current scan sometimes took down sites. + Worst case scenario is when a user just clicked "ok" without entering a username. This could take down the entire website. + This is related to the ruby (language) bug: + http://rubyforge.org/tracker/index.php?func=detail&aid=21131&group_id=426&atid=1698 + +Tue Jul 1 22:59:09 2008 +0200 Christoffer Sawicki + * spec_rack_handler.rb - Fixed typos + +Mon Jun 23 17:18:28 2008 +0200 Christoffer Sawicki + * spec_rack_utils.rb - Reformulated two test case descriptions + +Sat Jul 5 02:23:45 2008 +0200 Christoffer Sawicki + * deflater.rb - Make gzip's mtime parameter mandatory + +Sat Jul 5 02:16:09 2008 +0200 Christoffer Sawicki + * deflater.rb - Update TODOs + +Sat Jul 5 02:13:17 2008 +0200 Christoffer Sawicki + * deflater.rb - Move out the Zlib::Deflate constructor arguments to a constant + +Fri Jul 4 23:57:05 2008 +0200 Christoffer Sawicki + * deflater.rb - Removed unnecessary require "time" and whitespace + +Fri Jul 4 12:53:43 2008 -0600 Ben Alpert + * added mtime for Deflater.gzip and fixed gzip spec + +Fri Jul 4 13:36:18 2008 +0200 Christoffer Sawicki + * deflater.rb - Added an error message for the 406 Not Acceptable case + +Fri Jul 4 02:35:15 2008 -0600 Ben + * added gzip support to Rack::Deflater + +Wed Jul 2 00:11:06 2008 +0200 Christoffer Sawicki + * Implemented Rack::Deflater + +Tue Jul 1 22:37:58 2008 +0200 Christoffer Sawicki + * Added support for Accept-Encoding (via Request#accept_encoding and Utils.select_best_encoding) + +Tue Jul 1 15:09:16 2008 -0700 Scytrin dai Kinthra + * Merge branch 'openid2' + +Tue Jul 1 15:02:00 2008 -0700 Scytrin dai Kinthra + * Refactoring of sanity checks on adding extensions for more descriptive exceptions. + Additional tests on extension handling. + +Tue Jul 1 14:58:27 2008 -0700 Scytrin dai Kinthra + * Default :return_to is Request#url. + Reordering of finish vs check to prevent recursive oid checks. + Additional $DEBUG output + +Tue Jul 1 14:55:37 2008 -0700 Scytrin dai Kinthra + * Documentation revisions. 80 cols! + +Sun Jun 29 13:37:27 2008 -0700 Scytrin dai Kinthra + * Additional documentation examples. + +Sun Jun 29 13:06:34 2008 -0700 Scytrin dai Kinthra + * Merge branch 'openid2' + +Sun Jun 29 13:05:05 2008 -0700 Scytrin dai Kinthra + * Revisions to setup checks in order to match test specs. + Revisions to corresponding documentation. + Addition of #extension_namespaces for conveniance. + +Sat Jun 28 17:54:45 2008 -0700 Scytrin dai Kinthra + * Additional checks and tests for extension handling. + +Sat Jun 28 17:19:07 2008 -0700 Scytrin dai Kinthra + * Expansion and better handling of extensions. + Additional documentation and revisions in reference to extensions. + General documentation revisions. + +Sat Jun 28 14:07:57 2008 -0700 Scytrin dai Kinthra + * Initial support for OpenID extensions. + Extensions require implementation from ::OpenID::Extension + +Sat Jun 28 14:37:09 2008 -0700 Scytrin dai Kinthra + * Reformatting of debug warning + +Fri Jun 27 09:44:38 2008 -0700 Dan Kubb + * Make Rack::File use RFC 2616 HTTP Date/Time format for Last-Modified + +Tue Jun 24 13:55:25 2008 +0200 Christian Neukirchen + * Merge commit 'scytrin/master' + +Tue Jun 24 11:57:04 2008 +0200 Christian Neukirchen + * Only call binmode when possible in the multipart parser + +Tue Jun 24 01:58:27 2008 -0700 Scytrin dai Kinthra + * Merge commit 'chneukirchen/master' + +Tue Jun 24 01:52:16 2008 -0700 Scytrin dai Kinthra + * Merge branch 'openid2' + +Tue Jun 24 01:43:03 2008 -0700 Scytrin dai Kinthra + * Documentation revisions + +Mon Jun 23 04:26:38 2008 -0700 Scytrin dai Kinthra + * OpenID2 moved to replace OpenID + +Fri Jun 20 23:16:21 2008 +0200 Christoffer Sawicki + * file.rb - Added MP3 to MIME_TYPES + +Mon Jun 23 04:25:10 2008 -0700 Scytrin dai Kinthra + * Merge branch 'openid2' + +Mon Jun 23 01:55:35 2008 -0700 Scytrin dai Kinthra + * Removed extraneous test file + Updated rubygems specification + +Mon Jun 23 01:04:10 2008 -0700 Scytrin dai Kinthra + * Addition of initial tests for OpenID2 + Additional checks on provided URIs. + +Sun Jun 22 08:27:48 2008 -0700 Scytrin dai Kinthra + * typo correction + +Sun Jun 22 01:09:49 2008 -0700 Scytrin dai Kinthra + * More rephrasing. + +Sat Jun 21 18:31:09 2008 -0700 Scytrin dai Kinthra + * Initial import of OpenID tests + +Sat Jun 21 18:29:45 2008 -0700 Scytrin dai Kinthra + * Revisions to check logic and presentation + +Sat Jun 21 18:10:27 2008 -0700 Scytrin dai Kinthra + * Documentation updates and revisions. + Addition of additional checks. + +Fri Jun 20 13:23:07 2008 -0700 Scytrin dai Kinthra + * Documentation update. + Removal of message appending in a cancel response. + +Fri Jun 20 12:56:05 2008 -0700 Scytrin dai Kinthra + * Documentation updates and improvements. + Adjusted naming for a few options. + The method #finish will always return a 303 redirect unless an error occurs. + +Wed Jun 18 03:57:23 2008 -0700 Scytrin dai Kinthra + * Inlining the management of exceptional responses. + Removal of extension support until assurance of a decent and clean way of support. + Revisions to documentation. + Rewriting of various expressions for clarity and consistancy. + Replaced hard coded symbols with constant reference. + Included list of optional arguments for notes on later documentation. + +Thu Jun 12 16:53:07 2008 -0700 Scytrin dai Kinthra + * Removed bare_login functionality, added an optional 500 returning intercept + +Mon Jun 2 23:37:45 2008 -0700 Scytrin dai Kinthra + * Removal of trailing whitespace + +Mon Jun 2 21:14:40 2008 -0700 Scytrin dai Kinthra + * Use $DEBUG for introspective output + +Sun May 25 19:26:55 2008 -0700 Scytrin dai Kinthra + * Make OpenID2 accessible by default + +Sun May 25 19:18:44 2008 -0700 Scytrin dai Kinthra + * Inclusion of ruby-openid 2.x compatible OpenID implementation + +Mon Jun 23 03:32:04 2008 -0700 Scytrin dai Kinthra + * Removal of extraneous debugging output + +Sun Jun 15 13:51:31 2008 +0200 Christian Neukirchen + * Check for block in Builder before instance_eval + +Thu Jun 12 17:17:24 2008 +0200 Christian Neukirchen + * Merge commit 'scytrin/master' + +Fri Jun 6 20:54:30 2008 -0700 Scytrin dai Kinthra + * Added documentation, checks, and tests for Rack::Utils::Context + +Fri Jun 6 17:25:35 2008 -0700 Adam Wiggins + * commonlogger passes through close call (fixes zombie process bug when serving popen responses) + +Tue Jun 3 21:55:55 2008 -0700 Scytrin dai Kinthra + * Reworking session/abstract/id and derived session implementations + Formatting for readability + Adjusted session-id finding for compatibility + Added checks, rescues, and debugging output + Adjusted and added tests + +Tue Jun 3 20:09:19 2008 -0700 Scytrin dai Kinthra + * Removal of lingering debug output in directory.rb + +Mon Jun 2 00:50:45 2008 -0700 Scytrin dai Kinthra + * Requiring socket stdlib for UNIXSocket and TCPSocket + +Sun Jun 1 06:34:14 2008 -0700 Scytrin dai Kinthra + * Tests for Rack::Directory, as well as removal of Rack::File dependency + +Sun Jun 1 06:07:44 2008 -0700 Scytrin dai Kinthra + * Addition of Directory to autoload index + +Sat May 31 14:32:34 2008 +0200 Christian Neukirchen + * Merge commit 'josh-mirror/master' + +Sat May 31 14:26:43 2008 +0200 Christian Neukirchen + * More cleanup + +Sat May 31 14:21:56 2008 +0200 Christian Neukirchen + * Mention Git repositories in README + +Sat May 31 14:09:31 2008 +0200 Christian Neukirchen + * Cleanup + +Mon May 26 09:12:22 2008 -0500 Joshua Peek + * Skip Camping and Memcache tests if the gems are not installed. + +Sun May 25 14:32:00 2008 +0000 Christian Neukirchen + * Add Rack.release for the version of the release. + +Sat May 24 17:54:49 2008 +0200 Christian Neukirchen + * Merge commit 'josh/master' + +Sat May 24 15:54:00 2008 +0000 Christian Neukirchen + * Allow handlers to register themselves with Rack::Handler. + +Sat May 24 09:57:09 2008 -0500 Joshua Peek + * Allow handlers to register themselves with Rack::Handler. + +Sat May 24 14:23:10 2008 +0200 Christian Neukirchen + * Merge commit '37c59dce25df4' + +Sat May 24 12:22:00 2008 +0000 Christian Neukirchen + * Merge walf443/rack-mirror + +Sat May 24 02:16:39 2008 +0900 Keiji, Yoshimi + * It may be better to show HTTP_X_FORWARDED_FOR if it exists. + It's useful when using reverse proxy in front of app server using Rack. + +Sun May 18 17:06:58 2008 +0200 Christian Neukirchen + * Merge commit 'josh/master' + +Sun May 18 15:05:00 2008 +0000 Christian Neukirchen + * Merge 'josh/rack-mirror' + +Sat May 17 15:39:16 2008 -0500 Joshua Peek + * Include EventedMongrel handler with Rack. + +Sat May 10 17:16:29 2008 +0200 Christian Neukirchen + * Merge commit 'josh/daemonize' + +Sat May 10 15:10:00 2008 +0000 Christian Neukirchen + * Merge josh/daemonize + +Tue May 6 18:14:47 2008 -0500 Joshua Peek + * Only write a rack pid if a file is given. + +Tue May 6 15:44:15 2008 -0500 Joshua Peek + * Added support for daemonizing servers started with rackup. + +Fri May 2 21:05:00 2008 +0000 Christoffer Sawicki + * utils.rb, spec_rack_utils.rb - Added build_query, the inverse of parse_query + +Fri May 2 20:53:00 2008 +0000 Christoffer Sawicki + * utils.rb - Cleaned up parse_query + +Fri May 2 21:04:00 2008 +0000 Christoffer Sawicki + * spec_rack_utils.rb - Added another test for parse_query + +Sat Apr 26 21:37:00 2008 +0000 Scytrin dai Kinthra + * session/abstract/id.rb - removal of gratuitous debug output + +Fri Apr 25 23:55:00 2008 +0000 Scytrin dai Kinthra + * directory.rb - serves html index for nonfile paths + + Rack::File similar processing of paths. On directory lookups it will serve + a html index of it's contents. Entries begining with '.' are not presented. + On lookups that result in a file, it will pass an unmodified env to the + provided app. If an app is not provided, a Rack::File with the same root is + used. + +Fri Apr 18 10:12:00 2008 +0000 Christian Neukirchen + * Open multipart tempfiles in binary mode + +Thu Apr 10 20:26:00 2008 +0000 ryan + * handle EOFError exception in Request#params + +Sat Mar 29 19:58:00 2008 +0000 Scytrin dai Kinthra + * utils.rb - addition of recontexting from a Context + +Sun May 25 14:33:00 2008 +0000 Christian Neukirchen + * Convert Rakefile to use Git + +Thu Mar 27 11:09:00 2008 +0000 Adam Harper + * Bug fix for Tempfile POST bodies under Ruby 1.8 + + The Tempfile class in Ruby 1.8 doesn't implement the == method correctly. + This causes Rack::Requests to re-parse the input (when the input is a + Tempfile) each time the POST method is called, this in turn raises an + EOFError because the input has already been read. + + One example of when this happens is when handling large POST requests + (e.g. file uploads) under Mongrel. + + This issue only effects Ruby 1.8 (tested against 1.8.6). Ruby 1.9 does + not suffer from this issue (presumably due to changes in the Delegate + implementation.) + +Sat Mar 29 04:32:00 2008 +0000 Scytrin dai Kinthra + * memcache.rb - Fixed immortal key bug, updated tests + + Old multithread behaviour was to merge sessions, which would never delete + keys, even if deleted in the current session. + +Tue Mar 25 11:15:00 2008 +0000 Scytrin dai Kinthra + * abstract/id.rb - Added check on correctness of response. + +Thu Mar 20 16:11:00 2008 +0000 Christian Neukirchen + * Run Rack::Session::Memcache tests in fulltest only + +Wed Mar 19 11:43:00 2008 +0000 Scytrin dai Kinthra + * memcache.rb - memcached based session management + +Thu Mar 20 16:06:00 2008 +0000 Christian Neukirchen + * Rack::Reloader is not loaded in rackup development mode anymore + +Tue Mar 18 04:04:00 2008 +0000 Scytrin dai Kinthra + * openid.rb - documentation and check on using ruby-openid 1.x.x + +Tue Mar 18 10:59:00 2008 +0000 Christian Neukirchen + * Update History + +Tue Mar 18 10:57:00 2008 +0000 Christian Neukirchen + * Update Rakefile + +Tue Mar 18 10:55:00 2008 +0000 Christian Neukirchen + * Make fulltest chmod the executables + +Tue Mar 18 10:54:00 2008 +0000 Christian Neukirchen + * Small README tweak + +Mon Mar 17 23:28:00 2008 +0000 stephen.bannasch + * Changes to get lighttpd setup and running when rake fulltest is run; also added some doc to the readme about running tests + +Mon Mar 17 16:03:00 2008 +0000 Scytrin dai Kinthra + * urlmap.rb - update test in allowance of non-destructive HeaderHash + +Mon Mar 17 15:59:00 2008 +0000 Scytrin dai Kinthra + * pool.rb - cleanup of session merging and threading collision checks + +Mon Mar 17 15:51:00 2008 +0000 Christian Neukirchen + * URLMap tweaks and more tests + +Mon Mar 17 15:51:00 2008 +0000 Christian Neukirchen + * Don't lose empty headers in HeaderHash + +Mon Mar 17 15:26:00 2008 +0000 Scytrin dai Kinthra + * urlmap.rb - alteration of path selection routines, with updated tests + + Previous implementation would append an extra '/' if PATH_NAME would otherwise + be empty. + +Mon Mar 17 11:19:00 2008 +0000 Scytrin dai Kinthra + * pool.rb - explicit require for thread stdlib + +Mon Mar 17 09:12:00 2008 +0000 Scytrin dai Kinthra + * pool.rb, id.rb - creation of abstract id based session handler + + Allows simpler implementation of various storage based sessioning. + More stringent type checks in id.rb + +Sun Mar 16 14:31:00 2008 +0000 Scytrin dai Kinthra + * updated and addition to tests for pool.rb for expiration and thread safety + + Running the freshness tests sleeps for 4 seconds to allow a session's + expiration point to pass. + +Sun Mar 16 14:30:00 2008 +0000 Scytrin dai Kinthra + * pool.rb - addition of session freshness check and upkeep routines + +Sun Mar 16 13:23:00 2008 +0000 Scytrin dai Kinthra + * utils.rb - Utils::Context - addition of introspection methods + +Sun Mar 16 11:55:00 2008 +0000 Scytrin dai Kinthra + * pool.rb - documentation update and collision warnings + +Sun Mar 16 09:01:00 2008 +0000 Scytrin dai Kinthra + * pool.rb - documentation revision, addition of @mutex accessor + +Sun Mar 16 08:33:00 2008 +0000 Scytrin dai Kinthra + * pool.rb - setup of expiry not using defined?, from apeiros + +Sun Mar 16 08:26:00 2008 +0000 Scytrin dai Kinthra + * pool.rb - saner metadata storage + +Sun Mar 16 08:23:00 2008 +0000 Scytrin dai Kinthra + * pool.rb - cleanup and THANKS + +Sun Mar 16 08:21:00 2008 +0000 Scytrin dai Kinthra + * pool.rb - addition of thread safety + +Sun Mar 16 04:59:00 2008 +0000 Scytrin dai Kinthra + * pool.rb - moved cookie building back to #commit_session + +Fri Mar 14 23:57:00 2008 +0000 Scytrin dai Kinthra + * pool.rb - faster session id generation from apeiros + +Mon Mar 17 10:56:00 2008 +0000 Christian Neukirchen + * Require time in rack/file.rb + + Reported by Stephen Bannasch. + +Sat Mar 15 13:51:00 2008 +0000 r + * Fix that Request assumes form-data even when Content-Type says otherwise + + Fixes cases where accessing Request#params causes the body to be read and + processed as form-data improperly. For example, consider the following + request: + + PUT /foo/bar?baz=bizzle HTTP/1.1 + Content-Type: text/plain + + This is not form-data. + + When Rack::Request gets ahold of the corresponding environment, and the + application attempts to access the "baz" query string param, the body is read + and interpreted as form-data. If the body is an IOish object, this causes the + offset to be forwarded to the end of file. + + The patch prevents the Request#POST method from going into the body unless the + media type is application/x-www-form-urlencoded, multipart/form-data, or not + specified. + + While here, added a few unrelated helper methods to Request that I've found + particularly useful in Rack apps: #content_length, #head?, and #content_charset. + + Tests and doc included for all changes. + +Tue Mar 11 12:02:00 2008 +0000 Scytrin dai Kinthra + * pool.rb - cleanup and portability revisions + +Tue Mar 11 11:59:00 2008 +0000 Scytrin dai Kinthra + * pool.rb - exported assignment of session id cookie + +Tue Mar 11 11:56:00 2008 +0000 Scytrin dai Kinthra + * pool.rb - exported session to pool commit + +Tue Mar 11 11:52:00 2008 +0000 Scytrin dai Kinthra + * pool.rb - altered session metadata storage and session commit point + +Tue Mar 11 11:29:00 2008 +0000 Scytrin dai Kinthra + * pool.rb - exported generation of a new session id + +Tue Mar 11 11:25:00 2008 +0000 Scytrin dai Kinthra + * pool.rb - moved inline hash to DEFAULT_OPTIONS + +Tue Mar 11 11:11:00 2008 +0000 Scytrin dai Kinthra + * pool.rb - removal of blocks for #context + + Before you could pass a block to Pool#context that would be passed the env + before performing a call on the passed app. This has been removed in deference + to the practice setting up the block as the passed app, which should + subsequently call the intended app. + Seems more in accordance with Rack's prescribed behaviour. + +Tue Mar 11 07:51:00 2008 +0000 Scytrin dai Kinthra + * Alteration of Mongrel.run for Mongrel based routing + + With the passing of the :map option Mongrel.run will handle the passing of a + Hash or URLMap rather than a standard rack app. The mapping provided by the + passed object will be used to register uris with the mongrel instance. + + Hashes should only have absolute paths for keys and apps for values. + + URLMaps will be filtered if the :Host options is specified, or the mapping's + host is set. + +Tue Mar 11 06:31:00 2008 +0000 Scytrin dai Kinthra + * Addition of #add, #<<, and #include? to Cascade, allowing iterative addition of apps + +Mon Mar 10 15:18:00 2008 +0000 Scytrin dai Kinthra + * Changed urlmap.rb's uri check to successive conditionals rather than one big one + +Tue Feb 26 12:28:00 2008 +0000 Christian Neukirchen + * Update README and docs + +Sun Feb 24 19:37:00 2008 +0000 Christian Neukirchen + * Don't use autoloads in the test suite + +Sun Feb 24 18:48:00 2008 +0000 Christian Neukirchen + * Fix test cases that used 201 as a status where Content-Type is not allowed + +Sun Feb 24 18:46:00 2008 +0000 Christian Neukirchen + * Fix cookie parsing + +Sun Feb 24 17:51:00 2008 +0000 Christian Neukirchen + * Let Rack::Builder#use accept blocks + + Contributed by Corey Jewett. + +Mon Feb 18 21:18:00 2008 +0000 Christian Neukirchen + * Don't create invalid header lines when only deleting a cookie + + Reported by Andreas Zehnder + +Sun Feb 3 17:14:00 2008 +0000 Christian Neukirchen + * Update lint to not check for 201 status headers + +Sun Feb 3 17:00:00 2008 +0000 Christian Neukirchen + * HTTP status 201 can contain a body + +Fri Jan 25 08:36:00 2008 +0000 Christian Neukirchen + * Add SCGI handler, by Jeremy Evans + +Tue Jan 22 04:23:00 2008 +0000 m.fellinger + * Fix syntax for toggle() in ShowExceptions + +Mon Jan 21 02:27:00 2008 +0000 Aman Gupta + * Conform to RFC 2109 regarding multiple values for same cookie + +Thu Jan 10 15:29:00 2008 +0000 Christian Neukirchen + * Remove Rack::Adapter::Rails autoload + +Mon Dec 31 18:34:00 2007 +0000 Christian Neukirchen + * Remove uses of base64 for Ruby 1.9 support + +Sun Dec 9 16:48:00 2007 +0000 Christian Neukirchen + * Make Rack::Lint actually check what the spec says. + +Sun Nov 18 20:09:00 2007 +0000 Scytrin dai Kinthra + * lib/rack/auth/openid.rb - typo! + +Sun Nov 18 20:03:00 2007 +0000 Scytrin dai Kinthra + * lib/rack/auth/openid.rb - updates to reflect rack styling + +Sun Nov 18 19:54:00 2007 +0000 Scytrin dai Kinthra + * lib/rack/auth/openid.rb - removal of block functionality + + The block argumentn functionality was causing a few complications and + was removed in favour of storing the openid status object in the + environment. A wrapping proc oor rack app can now achieve the same + functionality as the block could, in a cleaner manner. + +Sun Nov 18 19:51:00 2007 +0000 Christian Neukirchen + * Small fix for the new FastCGI options + +Sun Nov 18 19:16:00 2007 +0000 Scytrin dai Kinthra + * lib/rack/urlmap.rb - Restyle of host matching from 'and' and 'or' to && and || + +Tue Aug 28 23:02:00 2007 +0000 Scytrin dai Kinthra + * Reformat and representation of mapping selection routine. + +Sun Nov 18 19:20:00 2007 +0000 Christian Neukirchen + * Minor tweaks in blink's code + +Sun Nov 18 18:45:00 2007 +0000 Scytrin dai Kinthra + * lib/rack/auth/openid.rb - removal of rubygems require + +Sun Nov 18 07:46:00 2007 +0000 Scytrin dai Kinthra + * lib/rack.rb - Addition of Auth::OpenID + +Sun Nov 18 07:45:00 2007 +0000 Scytrin dai Kinthra + * lib/rack.rb - Addition of new Session::Pool and Memcache + +Sun Nov 18 05:08:00 2007 +0000 Scytrin dai Kinthra + * session/pool.rb - Updated to use Rack::Utils::Context + +Sun Nov 18 04:57:00 2007 +0000 Scytrin dai Kinthra + * Inclusion of the openid result for the post-run block + +Sun Nov 18 04:54:00 2007 +0000 Scytrin dai Kinthra + * Addition of post-run block for extensibility + +Sun Nov 18 04:53:00 2007 +0000 Scytrin dai Kinthra + * Addition of request to provide a default return url + +Sun Nov 18 04:50:00 2007 +0000 Scytrin dai Kinthra + * Cleanup of code, errant error call + +Sun Nov 18 04:45:00 2007 +0000 Scytrin dai Kinthra + * Addition of Rack::Utils::Context + + Allows the use of a rack app in different contexts using a proc. + +Sun Nov 18 04:42:00 2007 +0000 Scytrin dai Kinthra + * Errors now method calls rather than constants. + +Thu Aug 30 13:30:00 2007 +0000 Scytrin dai Kinthra + * addition of js -> text/javascript to file types + +Thu Aug 30 13:28:00 2007 +0000 Scytrin dai Kinthra + * addition of Last-Modified http header to Rack::File + +Tue Aug 28 23:14:00 2007 +0000 Scytrin dai Kinthra + * Addition of credits, #for to allow app context change, and addition of a #key accessor + +Wed Aug 22 04:17:00 2007 +0000 Scytrin dai Kinthra + * lib/rack/handler/fastcgi.rb - :Port and :File options for opening sockets + +Fri Aug 17 07:09:00 2007 +0000 Scytrin dai Kinthra + * lib/rack/auth/openid.rb: openid login authenticator + +Thu Nov 15 16:21:00 2007 +0000 Christian Neukirchen + * Fix SCRIPT_NAME in nested URLMaps + +Thu Nov 15 16:20:00 2007 +0000 Christian Neukirchen + * Update AUTHORS and thanks + +Thu Nov 15 16:11:00 2007 +0000 Christian Neukirchen + * Fix warning + +Thu Nov 15 16:10:00 2007 +0000 Christian Neukirchen + * Make Rack::Builder#to_app nondestructive + +Tue Oct 9 14:35:00 2007 +0000 Christian Neukirchen + * Fix Cookie dates accordingly to RFC 2109 + +Wed Sep 12 09:15:00 2007 +0000 Christian Neukirchen + * Mention PUT as allowed request method in the spec + +Sat Aug 11 17:28:00 2007 +0000 Scytrin dai Kinthra + * pool.rb - local session storage hash pool w/ tests + +Thu Jul 12 09:02:00 2007 +0000 Christian Neukirchen + * Add LiteSpeed handler + + Courtesy of Adrian Madrid + +Thu Jun 14 20:34:00 2007 +0000 Christoffer Sawicki + * Make Rack::File serve files with URL encoded filenames + +Thu May 31 16:36:00 2007 +0000 Christian Neukirchen + * Make Rack::Response possibly close the body + + Proposed by Jonathan Buch + +Thu May 17 12:06:00 2007 +0000 Christian Neukirchen + * Better running of lighttpd for testing + +Wed May 16 17:34:00 2007 +0000 Christian Neukirchen + * Credit Luc Heinrich + +Wed May 16 15:01:00 2007 +0000 Christian Neukirchen + * Different approach to Mongrel#run testing + +Wed May 16 14:53:00 2007 +0000 Christian Neukirchen + * Fix trailing whitespace. Sigh. + +Wed May 16 14:44:00 2007 +0000 Christian Neukirchen + * Update README + +Wed May 16 14:43:00 2007 +0000 Christian Neukirchen + * Yield the servers optionally + +Wed May 16 14:32:00 2007 +0000 Christian Neukirchen + * Small docfixes + +Tue May 15 23:44:00 2007 +0000 Michael Fellinger + * replace the 'system' calls in Rakefile with 'sh', making them more transparent and --trace able + +Tue May 15 23:42:00 2007 +0000 Michael Fellinger + * add some features to Request and the corresponding tests for them + +Tue May 15 15:43:00 2007 +0000 Christian Neukirchen + * Make Rack::Handler::*.run yield the server for further configuration + +Fri May 11 15:31:00 2007 +0000 Christian Neukirchen + * Remove the Rails adapter, it was never useful + +Fri May 11 15:12:00 2007 +0000 Christian Neukirchen + * Introduce Rack::Response::Helpers and make MockResponse use them, too. + +Fri May 11 14:56:00 2007 +0000 Christian Neukirchen + * Add some more edge-case tests to improve coverage + +Sun Apr 29 12:55:00 2007 +0000 Christoffer Sawicki + * Add missing autoload for Cascade in rack.rb + +Thu Apr 26 14:05:00 2007 +0000 Christian Neukirchen + * Make ShowStatus more robust + +Wed Apr 18 13:15:00 2007 +0000 Christian Neukirchen + * Add Rack::Response#empty? + +Tue Apr 3 20:59:00 2007 +0000 Tim Fletcher + * Minor tweaks + +Tue Apr 3 20:58:00 2007 +0000 Tim Fletcher + * Some initial documentation for the main authentication classes + +Tue Apr 3 20:56:00 2007 +0000 Tim Fletcher + * An example of how to use Rack::Auth::Basic. Protect your lobsters! + +Tue Apr 3 20:17:00 2007 +0000 Tim Fletcher + * Make Rack::Auth handlers compatible with Rack::ShowStatus + +Tue Apr 3 20:09:00 2007 +0000 Tim Fletcher + * Ensure Rack::ShowStatus passes on headers + +Fri Mar 30 13:12:00 2007 +0000 Christian Neukirchen + * Add Request#fullpath + +Thu Mar 29 14:24:00 2007 +0000 Christian Neukirchen + * Add Rack::ShowStatus, a filter to generate common error messages + +Thu Mar 29 14:20:00 2007 +0000 Christian Neukirchen + * Add a list of HTTP status messages + +Tue Mar 27 09:06:00 2007 +0000 Christian Neukirchen + * Small cleanup + +Mon Mar 26 21:27:00 2007 +0000 Tim Fletcher + * Adding Rack::Auth::Digest::MD5, and refactoring Auth::Basic accordingly + +Sat Mar 24 14:36:00 2007 +0000 Christian Neukirchen + * Doc fix, Request should have been Reponse + + Thanks, apeiros + +Mon Mar 12 16:45:00 2007 +0000 Christian Neukirchen + * Add a test for the broken cookie sessions + +Mon Mar 12 16:04:00 2007 +0000 luc + * Make sure we get a valid empty session hash in all cases. + +Sun Mar 11 14:06:00 2007 +0000 Christian Neukirchen + * Integrate Rack::Static + +Sun Mar 11 14:04:00 2007 +0000 Christian Neukirchen + * Ducktype on #to_str for Rack::Response.new + + proposed by Gary Wright + +Sun Mar 11 13:43:00 2007 +0000 luc + * Added Rack::Static middleware. + +Sun Mar 11 13:50:00 2007 +0000 Christian Neukirchen + * Make Rack::Response#write call #to_s + + proposed by Gary Wright + +Sat Mar 10 14:38:00 2007 +0000 Christian Neukirchen + * Fix Rack::Session::Cookie + +Fri Mar 9 23:40:00 2007 +0000 luc + * Cookie based session management middleware. + +Tue Mar 6 21:12:00 2007 +0000 Christian Neukirchen + * Load pp when debugging + +Tue Mar 6 12:19:00 2007 +0000 Christian Neukirchen + * Integrate patches + +Sun Mar 4 15:12:00 2007 +0000 Tim Fletcher + * Adding Rack::Auth::Basic + +Sun Mar 4 02:29:00 2007 +0000 Aredridel + * Fix Camping redirects into Strings when they're URIs + +Sat Mar 3 17:20:00 2007 +0000 Christian Neukirchen + * Fix things that should have been fixed before the release *sigh* + +Sat Mar 3 12:40:00 2007 +0000 Christian Neukirchen + * Fix CGI permissions + +Sat Mar 3 12:34:00 2007 +0000 Christian Neukirchen + * Last-minute details + +Sat Mar 3 11:15:00 2007 +0000 Christian Neukirchen + * Extend gemspec + +Sat Mar 3 10:37:00 2007 +0000 Christian Neukirchen + * Small README fixes + +Sat Mar 3 10:16:00 2007 +0000 Christian Neukirchen + * Add README and other documentation + +Sat Mar 3 09:58:00 2007 +0000 Christian Neukirchen + * Add and integrate Rakefile + +Sat Mar 3 09:56:00 2007 +0000 Christian Neukirchen + * Add some missing tests + +Fri Mar 2 23:53:00 2007 +0000 Christoffer Sawicki + * Tidy up RailsDispatcher::CGIStub + +Fri Mar 2 16:55:00 2007 +0000 Christian Neukirchen + * Handle SCRIPT_NAME better in *CGI environments + +Fri Mar 2 15:10:00 2007 +0000 Christian Neukirchen + * Remove lighttpd comment. + + The bug has been fixed in later versions. + +Thu Mar 1 18:53:00 2007 +0000 Christian Neukirchen + * Add RDocs + +Wed Feb 28 22:19:00 2007 +0000 Christoffer Sawicki + * Make Adapter::Rails use Cascade + +Wed Feb 28 20:06:00 2007 +0000 Christian Neukirchen + * Fix warnings + +Wed Feb 28 20:03:00 2007 +0000 Christian Neukirchen + * Add Rack::Cascade, to pass on the first non 404 result + +Wed Feb 28 19:12:00 2007 +0000 Christian Neukirchen + * Move TestRequest to test/ + +Wed Feb 28 19:09:00 2007 +0000 Christian Neukirchen + * Make spec_rack_lint.rb use mocks + +Wed Feb 28 18:56:00 2007 +0000 Christian Neukirchen + * Make spec_rack_camping.rb use mocks + +Wed Feb 28 18:55:00 2007 +0000 Christian Neukirchen + * Make spec_rack_urlmap.rb use mocks + +Wed Feb 28 18:30:00 2007 +0000 Christian Neukirchen + * Make spec_rack_showexceptions.rb use mocks + +Wed Feb 28 18:26:00 2007 +0000 Christian Neukirchen + * Make spec_rack_request.rb use mocks + +Wed Feb 28 18:25:00 2007 +0000 Christian Neukirchen + * Don't clash constants in specifications + +Wed Feb 28 18:21:00 2007 +0000 Christian Neukirchen + * MockRequest can now only create the Rack environment + +Wed Feb 28 18:13:00 2007 +0000 Christian Neukirchen + * Make spec_rack_recursive.rb use mocks + +Wed Feb 28 17:50:00 2007 +0000 Christian Neukirchen + * Add a default SCRIPT_NAME + +Wed Feb 28 17:44:00 2007 +0000 Christian Neukirchen + * Make spec_rack_file.rb use mocks + +Wed Feb 28 17:40:00 2007 +0000 Christian Neukirchen + * Make spec_rack_commonlogger.rb use mocks + +Wed Feb 28 17:35:00 2007 +0000 Christian Neukirchen + * Add support for mocking all request methods + +Wed Feb 28 17:29:00 2007 +0000 Christian Neukirchen + * Add MockRequest/MockResponse for easier testing + +Wed Feb 28 13:45:00 2007 +0000 Christian Neukirchen + * Remove the port number of HTTP_HOST and SERVER_NAME + +Wed Feb 28 13:33:00 2007 +0000 Christian Neukirchen + * Make multipart reading more robust + +Wed Feb 28 12:56:00 2007 +0000 Christian Neukirchen + * Make Rack::Request read multipart form data + +Wed Feb 28 12:56:00 2007 +0000 Christian Neukirchen + * Allow rack.input.read(integer), needed for safe multipart parsing + +Mon Feb 26 18:45:00 2007 +0000 Christian Neukirchen + * Add CGI and FastCGI support for rackup + +Mon Feb 26 18:42:00 2007 +0000 Christian Neukirchen + * Make *CGI#run really like the others + +Mon Feb 26 18:10:00 2007 +0000 Christian Neukirchen + * Adapt Rack::Handler::CGI API + +Mon Feb 26 17:59:00 2007 +0000 Christian Neukirchen + * Add a FastCGI handler + +Sun Feb 25 21:14:00 2007 +0000 Christian Neukirchen + * Make Rack::Response#write return the written string to catch errors with nested writes + +Sun Feb 25 15:49:00 2007 +0000 Christian Neukirchen + * Add Rack::Reloader, a code autoreloader + +Sun Feb 25 13:46:00 2007 +0000 Christian Neukirchen + * Ensure the Response body is writable + +Sun Feb 25 13:40:00 2007 +0000 Christian Neukirchen + * Improve the Rack::Response constructor + + based on a patch from mitsuhiko. + +Sun Feb 25 12:24:00 2007 +0000 Christian Neukirchen + * Add the official logo + +Sat Feb 24 18:03:00 2007 +0000 Christian Neukirchen + * Add rackup, an experimental standalone Rack app starter + +Sat Feb 24 18:02:00 2007 +0000 Christian Neukirchen + * Add Rack::Builder, a DSL for connecting Rack apps + +Sat Feb 24 18:01:00 2007 +0000 Christian Neukirchen + * Really fix URLMap + +Thu Feb 22 20:35:00 2007 +0000 Christian Neukirchen + * Lint fix + +Thu Feb 22 20:34:00 2007 +0000 Christian Neukirchen + * Route root app correctly in URLMap + +Thu Feb 22 11:10:00 2007 +0000 Christian Neukirchen + * Add tests for Request#query_string + +Wed Feb 21 22:25:00 2007 +0000 Christoffer Sawicki + * Add getter method for the query string (and use it internally) + +Wed Feb 21 17:29:00 2007 +0000 Christoffer Sawicki + * Extended CGIStub to handle Rails' session cookie + +Wed Feb 21 19:23:00 2007 +0000 Christian Neukirchen + * Add a first draft of the specification to Rack::Lint + +Wed Feb 21 18:49:00 2007 +0000 Christian Neukirchen + * Ensure the body is closed + +Wed Feb 21 17:46:00 2007 +0000 Christian Neukirchen + * Add AUTHORS + +Wed Feb 21 16:49:00 2007 +0000 Christoffer Sawicki + * Basic Rails handler for Rack + +Wed Feb 21 17:03:00 2007 +0000 Christian Neukirchen + * Add Request#url + +Wed Feb 21 16:41:00 2007 +0000 Christian Neukirchen + * Fix extension->MIME mapping + +Wed Feb 21 15:13:00 2007 +0000 Christian Neukirchen + * Add Rack::Recursive and ForwardRequest + +Wed Feb 21 15:11:00 2007 +0000 Christian Neukirchen + * URLMap should only look at PATH_INFO + +Tue Feb 20 18:15:00 2007 +0000 Christian Neukirchen + * Call body#close if possible + +Mon Feb 19 12:19:00 2007 +0000 Christian Neukirchen + * Small exception handler tweak + +Mon Feb 19 11:22:00 2007 +0000 Christian Neukirchen + * Return empty hash on lack of cookies + +Mon Feb 19 11:22:00 2007 +0000 Christian Neukirchen + * Fix host dispatching with explicit ports + +Mon Feb 19 10:23:00 2007 +0000 Christian Neukirchen + * Cache the parsed things in Rack::Request + +Sun Feb 18 23:23:00 2007 +0000 Christian Neukirchen + * Rename Request#method to #request_method to not confuse stdlibs + +Sun Feb 18 23:02:00 2007 +0000 Christian Neukirchen + * Add Camping adapter autoload + +Sun Feb 18 22:52:00 2007 +0000 Christian Neukirchen + * Put Rack under the MIT license + +Sun Feb 18 18:07:00 2007 +0000 Christian Neukirchen + * Add Rack::CommonLogger, a Common Log Format request logger + +Sun Feb 18 17:52:00 2007 +0000 Christian Neukirchen + * Make Response#status and #body settable + +Sun Feb 18 10:50:00 2007 +0000 Christian Neukirchen + * More convenience for Rack::Request + +Sat Feb 17 13:49:00 2007 +0000 Christian Neukirchen + * Remove trailing whitespace *sigh* + +Sat Feb 17 13:46:00 2007 +0000 Christian Neukirchen + * Add Rack::URLMap, a simple router + +Sat Feb 17 13:04:00 2007 +0000 Christian Neukirchen + * Remove Python leftover + +Sat Feb 17 12:57:00 2007 +0000 Christian Neukirchen + * Add a Camping adapter + +Sat Feb 17 12:57:00 2007 +0000 Christian Neukirchen + * Don't define path_info twice + +Sat Feb 17 12:56:00 2007 +0000 Christian Neukirchen + * Add Rack::ShowExceptions + +Sat Feb 17 12:55:00 2007 +0000 Christian Neukirchen + * Remove stray paths + +Fri Feb 16 16:54:00 2007 +0000 Christian Neukirchen + * Add lobster version with Request/Response + +Fri Feb 16 16:53:00 2007 +0000 Christian Neukirchen + * Make Rack::Response#write syncronous + +Fri Feb 16 16:42:00 2007 +0000 Christian Neukirchen + * Add more Rack::Utils specs + +Fri Feb 16 16:34:00 2007 +0000 Christian Neukirchen + * Add Rack::Response and Rack::Utils + +Fri Feb 16 15:32:00 2007 +0000 Christian Neukirchen + * Add Rack::Request + +Fri Feb 16 15:30:00 2007 +0000 Christian Neukirchen + * Add Rack::File, a static file server + +Fri Feb 16 14:51:00 2007 +0000 Christian Neukirchen + * Move testing helpers to TestRequest + +Fri Feb 16 13:40:00 2007 +0000 Christian Neukirchen + * Add a lobster + +Fri Feb 16 13:39:00 2007 +0000 Christian Neukirchen + * Add rack.rb with autoloads for convenience + +Fri Feb 16 13:33:00 2007 +0000 Christian Neukirchen + * Add quick run methods for WEBrick and Mongrel + +Fri Feb 16 13:27:00 2007 +0000 Christian Neukirchen + * Fix lint to allow empty SCRIPT_NAME and PATH_INFO + +Fri Feb 16 13:01:00 2007 +0000 Christian Neukirchen + * Add Lint to the tests + +Fri Feb 16 12:49:00 2007 +0000 Christian Neukirchen + * Add Rack::Lint + +Thu Feb 15 18:05:00 2007 +0000 Christian Neukirchen + * Initial import of Rack + diff --git a/vendor/rack-0.9.1/KNOWN-ISSUES b/vendor/rack-0.9.1/KNOWN-ISSUES new file mode 100644 index 00000000..790199bd --- /dev/null +++ b/vendor/rack-0.9.1/KNOWN-ISSUES @@ -0,0 +1,18 @@ += Known issues with Rack and Web servers + +* Lighttpd sets wrong SCRIPT_NAME and PATH_INFO if you mount your + FastCGI app at "/". This can be fixed by using this middleware: + + class LighttpdScriptNameFix + def initialize(app) + @app = app + end + + def call(env) + env["PATH_INFO"] = env["SCRIPT_NAME"].to_s + env["PATH_INFO"].to_s + env["SCRIPT_NAME"] = "" + @app.call(env) + end + end + + Of course, use this only when your app runs at "/". diff --git a/vendor/rack-0.9.1/README b/vendor/rack-0.9.1/README new file mode 100644 index 00000000..a4f45ab4 --- /dev/null +++ b/vendor/rack-0.9.1/README @@ -0,0 +1,306 @@ += Rack, a modular Ruby webserver interface + +Rack provides a minimal, modular and adaptable interface for developing +web applications in Ruby. By wrapping HTTP requests and responses in +the simplest way possible, it unifies and distills the API for web +servers, web frameworks, and software in between (the so-called +middleware) into a single method call. + +The exact details of this are described in the Rack specification, +which all Rack applications should conform to. + +== Supported web servers + +The included *handlers* connect all kinds of web servers to Rack: +* Mongrel +* EventedMongrel +* SwiftipliedMongrel +* WEBrick +* FCGI +* CGI +* SCGI +* LiteSpeed +* Thin + +These web servers include Rack handlers in their distributions: +* Ebb +* Fuzed +* Phusion Passenger (which is mod_rack for Apache) + +Any valid Rack app will run the same on all these handlers, without +changing anything. + +== Supported web frameworks + +The included *adapters* connect Rack with existing Ruby web frameworks: +* Camping + +These frameworks include Rack adapters in their distributions: +* Coset +* Halcyon +* Mack +* Maveric +* Merb +* Racktools::SimpleApplication +* Ramaze +* Ruby on Rails +* Sinatra +* Sin +* Vintage +* Waves + +Current links to these projects can be found at +http://ramaze.net/#other-frameworks + +== Available middleware + +Between the server and the framework, Rack can be customized to your +applications needs using middleware, for example: +* Rack::URLMap, to route to multiple applications inside the same process. +* Rack::CommonLogger, for creating Apache-style logfiles. +* Rack::ShowException, for catching unhandled exceptions and + presenting them in a nice and helpful way with clickable backtrace. +* Rack::File, for serving static files. +* ...many others! + +All these components use the same interface, which is described in +detail in the Rack specification. These optional components can be +used in any way you wish. + +== Convenience + +If you want to develop outside of existing frameworks, implement your +own ones, or develop middleware, Rack provides many helpers to create +Rack applications quickly and without doing the same web stuff all +over: +* Rack::Request, which also provides query string parsing and + multipart handling. +* Rack::Response, for convenient generation of HTTP replies and + cookie handling. +* Rack::MockRequest and Rack::MockResponse for efficient and quick + testing of Rack application without real HTTP round-trips. + +== rack-contrib + +The plethora of useful middleware created the need for a project that +collects fresh Rack middleware. rack-contrib includes a variety of +add-on components for Rack and it is easy to contribute new modules. + +* http://github.com/rack/rack-contrib + +== rackup + +rackup is a useful tool for running Rack applications, which uses the +Rack::Builder DSL to configure middleware and build up applications +easily. + +rackup automatically figures out the environment it is run in, and +runs your application as FastCGI, CGI, or standalone with Mongrel or +WEBrick---all from the same configuration. + +== Quick start + +Try the lobster! + +Either with the embedded WEBrick starter: + + ruby -Ilib lib/rack/lobster.rb + +Or with rackup: + + bin/rackup -Ilib example/lobster.ru + +By default, the lobster is found at http://localhost:9292. + +== Installing with RubyGems + +A Gem of Rack is available. You can install it with: + + gem install rack + +I also provide a local mirror of the gems (and development snapshots) +at my site: + + gem install rack --source http://chneukirchen.org/releases/gems/ + +== Running the tests + +Testing Rack requires the test/spec testing framework: + + gem install test-spec + +There are two rake-based test tasks: + + rake test tests all the fast tests (no Handlers or Adapters) + rake fulltest runs all the tests + +The fast testsuite has no dependencies outside of the core Ruby +installation and test-spec. + +To run the test suite completely, you need: + + * camping + * mongrel + * fcgi + * ruby-openid + * memcache-client + +The full set of tests test FCGI access with lighttpd (on port +9203) so you will need lighttpd installed as well as the FCGI +libraries and the fcgi gem: + +Download and install lighttpd: + + http://www.lighttpd.net/download + +Installing the FCGI libraries: + + curl -O http://www.fastcgi.com/dist/fcgi-2.4.0.tar.gz + tar xzvf fcgi-2.4.0.tar.gz + cd fcgi-2.4.0 + ./configure --prefix=/usr/local + make + sudo make install + cd .. + +Installing the Ruby fcgi gem: + + gem install fcgi + +Furthermore, to test Memcache sessions, you need memcached (will be +run on port 11211) and memcache-client installed. + +== History + +* March 3rd, 2007: First public release 0.1. + +* May 16th, 2007: Second public release 0.2. + * HTTP Basic authentication. + * Cookie Sessions. + * Static file handler. + * Improved Rack::Request. + * Improved Rack::Response. + * Added Rack::ShowStatus, for better default error messages. + * Bug fixes in the Camping adapter. + * Removed Rails adapter, was too alpha. + +* February 26th, 2008: Third public release 0.3. + * LiteSpeed handler, by Adrian Madrid. + * SCGI handler, by Jeremy Evans. + * Pool sessions, by blink. + * OpenID authentication, by blink. + * :Port and :File options for opening FastCGI sockets, by blink. + * Last-Modified HTTP header for Rack::File, by blink. + * Rack::Builder#use now accepts blocks, by Corey Jewett. + (See example/protectedlobster.ru) + * HTTP status 201 can contain a Content-Type and a body now. + * Many bugfixes, especially related to Cookie handling. + +* August 21st, 2008: Fourth public release 0.4. + * New middleware, Rack::Deflater, by Christoffer Sawicki. + * OpenID authentication now needs ruby-openid 2. + * New Memcache sessions, by blink. + * Explicit EventedMongrel handler, by Joshua Peek + * Rack::Reloader is not loaded in rackup development mode. + * rackup can daemonize with -D. + * Many bugfixes, especially for pool sessions, URLMap, thread safety + and tempfile handling. + * Improved tests. + * Rack moved to Git. + +* January 6th, 2009: Fifth public release 0.9. + * Rack is now managed by the Rack Core Team. + * Rack::Lint is stricter and follows the HTTP RFCs more closely. + * Added ConditionalGet middleware. + * Added ContentLength middleware. + * Added Deflater middleware. + * Added Head middleware. + * Added MethodOverride middleware. + * Rack::Mime now provides popular MIME-types and their extension. + * Mongrel Header now streams. + * Added Thin handler. + * Official support for swiftiplied Mongrel. + * Secure cookies. + * Made HeaderHash case-preserving. + * Many bugfixes and small improvements. + +* January 9th, 2009: Sixth public release 0.9.1. + * Fix directory traversal exploits in Rack::File and Rack::Directory. + +== Contact + +Please mail bugs, suggestions and patches to +. + +Mailing list archives are available at +. + +There is a bug tracker at . + +Git repository (patches rebased on master are most welcome): +* http://github.com/rack/rack +* http://git.vuxu.org/cgi-bin/gitweb.cgi?p=rack.git + +You are also welcome to join the #rack channel on irc.freenode.net. + +== Thanks + +The Rack Core Team, consisting of + +* Christian Neukirchen (chneukirchen) +* James Tucker (raggi) +* Josh Peek (josh) +* Michael Fellinger (manveru) +* Ryan Tomayko (rtomayko) +* Scytrin dai Kinthra (scytrin) + +would like to thank: + +* Adrian Madrid, for the LiteSpeed handler. +* Christoffer Sawicki, for the first Rails adapter and Rack::Deflater. +* Tim Fletcher, for the HTTP authentication code. +* Luc Heinrich for the Cookie sessions, the static file handler and bugfixes. +* Armin Ronacher, for the logo and racktools. +* Aredridel, Ben Alpert, Dan Kubb, Daniel Roethlisberger, Matt Todd, + Tom Robinson, and Phil Hagelberg for bug fixing and other + improvements. +* Stephen Bannasch, for bug reports and documentation. +* Gary Wright, for proposing a better Rack::Response interface. +* Jonathan Buch, for improvements regarding Rack::Response. +* Armin Röhrl, for tracking down bugs in the Cookie generator. +* Alexander Kellett for testing the Gem and reviewing the announcement. +* Marcus Rückert, for help with configuring and debugging lighttpd. +* The WSGI team for the well-done and documented work they've done and + Rack builds up on. +* All bug reporters and patch contributers not mentioned above. + +== Copyright + +Copyright (C) 2007, 2008, 2009 Christian Neukirchen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +== Links + +Rack:: +Rack's Rubyforge project:: +Official Rack repositories:: +rack-devel mailing list:: + +Christian Neukirchen:: + diff --git a/vendor/rack-0.9.1/Rakefile b/vendor/rack-0.9.1/Rakefile new file mode 100644 index 00000000..5f3da93c --- /dev/null +++ b/vendor/rack-0.9.1/Rakefile @@ -0,0 +1,188 @@ +# Rakefile for Rack. -*-ruby-*- +require 'rake/rdoctask' +require 'rake/testtask' + + +desc "Run all the tests" +task :default => [:test] + +desc "Do predistribution stuff" +task :predist => [:chmod, :changelog, :rdoc] + + +desc "Make an archive as .tar.gz" +task :dist => [:fulltest, :predist] do + sh "git archive --format=tar --prefix=#{release}/ HEAD^{tree} >#{release}.tar" + sh "pax -waf #{release}.tar -s ':^:#{release}/:' RDOX SPEC ChangeLog doc" + sh "gzip -f -9 #{release}.tar" +end + +# Helper to retrieve the "revision number" of the git tree. +def git_tree_version + if File.directory?(".git") + @tree_version ||= `git describe`.strip.sub('-', '.') + @tree_version << ".0" unless @tree_version.count('.') == 2 + else + $: << "lib" + require 'rack' + @tree_version = Rack.release + end + @tree_version +end + +def gem_version + git_tree_version.gsub(/-.*/, '') +end + +def release + "rack-#{git_tree_version}" +end + +def manifest + `git ls-files`.split("\n") +end + + +desc "Make binaries executable" +task :chmod do + Dir["bin/*"].each { |binary| File.chmod(0775, binary) } + Dir["test/cgi/test*"].each { |binary| File.chmod(0775, binary) } +end + +desc "Generate a ChangeLog" +task :changelog do + File.open("ChangeLog", "w") { |out| + `git log -z`.split("\0").map { |chunk| + author = chunk[/Author: (.*)/, 1].strip + date = chunk[/Date: (.*)/, 1].strip + desc, detail = $'.strip.split("\n", 2) + detail ||= "" + detail = detail.gsub(/.*darcs-hash:.*/, '') + detail.rstrip! + out.puts "#{date} #{author}" + out.puts " * #{desc.strip}" + out.puts detail unless detail.empty? + out.puts + } + } +end + + +desc "Generate RDox" +task "RDOX" do + sh "specrb -Ilib:test -a --rdox >RDOX" +end + +desc "Generate Rack Specification" +task "SPEC" do + File.open("SPEC", "wb") { |file| + IO.foreach("lib/rack/lint.rb") { |line| + if line =~ /## (.*)/ + file.puts $1 + end + } + } +end + +desc "Run all the fast tests" +task :test do + sh "specrb -Ilib:test -w #{ENV['TEST'] || '-a'} #{ENV['TESTOPTS'] || '-t "^(?!Rack::Handler|Rack::Adapter|Rack::Session::Memcache|Rack::Auth::OpenID)"'}" +end + +desc "Run all the tests" +task :fulltest => [:chmod] do + sh "specrb -Ilib:test -w #{ENV['TEST'] || '-a'} #{ENV['TESTOPTS']}" +end + +begin + require 'rubygems' + + require 'rake' + require 'rake/clean' + require 'rake/packagetask' + require 'rake/gempackagetask' + require 'fileutils' +rescue LoadError + # Too bad. +else + spec = Gem::Specification.new do |s| + s.name = "rack" + s.version = gem_version + s.platform = Gem::Platform::RUBY + s.summary = "a modular Ruby webserver interface" + + s.description = <<-EOF +Rack provides minimal, modular and adaptable interface for developing +web applications in Ruby. By wrapping HTTP requests and responses in +the simplest way possible, it unifies and distills the API for web +servers, web frameworks, and software in between (the so-called +middleware) into a single method call. + +Also see http://rack.rubyforge.org. + EOF + + s.files = manifest + %w(SPEC RDOX) + s.bindir = 'bin' + s.executables << 'rackup' + s.require_path = 'lib' + s.has_rdoc = true + s.extra_rdoc_files = ['README', 'SPEC', 'RDOX', 'KNOWN-ISSUES'] + s.test_files = Dir['test/{test,spec}_*.rb'] + + s.author = 'Christian Neukirchen' + s.email = 'chneukirchen@gmail.com' + s.homepage = 'http://rack.rubyforge.org' + s.rubyforge_project = 'rack' + + s.add_development_dependency 'test-spec' + + s.add_development_dependency 'camping' + s.add_development_dependency 'fcgi' + s.add_development_dependency 'memcache-client' + s.add_development_dependency 'mongrel' + s.add_development_dependency 'ruby-openid', '~> 2.0.0' + s.add_development_dependency 'thin' + end + + Rake::GemPackageTask.new(spec) do |p| + p.gem_spec = spec + p.need_tar = false + p.need_zip = false + end +end + +desc "Generate RDoc documentation" +Rake::RDocTask.new(:rdoc) do |rdoc| + rdoc.options << '--line-numbers' << '--inline-source' << + '--main' << 'README' << + '--title' << 'Rack Documentation' << + '--charset' << 'utf-8' + rdoc.rdoc_dir = "doc" + rdoc.rdoc_files.include 'README' + rdoc.rdoc_files.include 'KNOWN-ISSUES' + rdoc.rdoc_files.include 'SPEC' + rdoc.rdoc_files.include 'RDOX' + rdoc.rdoc_files.include('lib/rack.rb') + rdoc.rdoc_files.include('lib/rack/*.rb') + rdoc.rdoc_files.include('lib/rack/*/*.rb') +end +task :rdoc => ["SPEC", "RDOX"] + +task :pushsite => [:rdoc] do + sh "rsync -avz doc/ chneukirchen@rack.rubyforge.org:/var/www/gforge-projects/rack/doc/" + sh "rsync -avz site/ chneukirchen@rack.rubyforge.org:/var/www/gforge-projects/rack/" +end + +begin + require 'rcov/rcovtask' + + Rcov::RcovTask.new do |t| + t.test_files = FileList['test/{spec,test}_*.rb'] + t.verbose = true # uncomment to see the executed command + t.rcov_opts = ["--text-report", + "-Ilib:test", + "--include-file", "^lib,^test", + "--exclude-only", "^/usr,^/home/.*/src,active_"] + end +rescue LoadError +end diff --git a/vendor/rack-0.9.1/SPEC b/vendor/rack-0.9.1/SPEC new file mode 100644 index 00000000..1f6c3434 --- /dev/null +++ b/vendor/rack-0.9.1/SPEC @@ -0,0 +1,129 @@ +This specification aims to formalize the Rack protocol. You +can (and should) use Rack::Lint to enforce it. +When you develop middleware, be sure to add a Lint before and +after to catch all mistakes. += Rack applications +A Rack application is an Ruby object (not a class) that +responds to +call+. +It takes exactly one argument, the *environment* +and returns an Array of exactly three values: +The *status*, +the *headers*, +and the *body*. +== The Environment +The environment must be an true instance of Hash (no +subclassing allowed) that includes CGI-like headers. +The application is free to modify the environment. +The environment is required to include these variables +(adopted from PEP333), except when they'd be empty, but see +below. +REQUEST_METHOD:: The HTTP request method, such as + "GET" or "POST". This cannot ever + be an empty string, and so is + always required. +SCRIPT_NAME:: The initial portion of the request + URL's "path" that corresponds to the + application object, so that the + application knows its virtual + "location". This may be an empty + string, if the application corresponds + to the "root" of the server. +PATH_INFO:: The remainder of the request URL's + "path", designating the virtual + "location" of the request's target + within the application. This may be an + empty string, if the request URL targets + the application root and does not have a + trailing slash. +QUERY_STRING:: The portion of the request URL that + follows the ?, if any. May be + empty, but is always required! +SERVER_NAME, SERVER_PORT:: When combined with SCRIPT_NAME and PATH_INFO, these variables can be used to complete the URL. Note, however, that HTTP_HOST, if present, should be used in preference to SERVER_NAME for reconstructing the request URL. SERVER_NAME and SERVER_PORT can never be empty strings, and so are always required. +HTTP_ Variables:: Variables corresponding to the + client-supplied HTTP request + headers (i.e., variables whose + names begin with HTTP_). The + presence or absence of these + variables should correspond with + the presence or absence of the + appropriate HTTP header in the + request. +In addition to this, the Rack environment must include these +Rack-specific variables: +rack.version:: The Array [0,1], representing this version of Rack. +rack.url_scheme:: +http+ or +https+, depending on the request URL. +rack.input:: See below, the input stream. +rack.errors:: See below, the error stream. +rack.multithread:: true if the application object may be simultaneously invoked by another thread in the same process, false otherwise. +rack.multiprocess:: true if an equivalent application object may be simultaneously invoked by another process, false otherwise. +rack.run_once:: true if the server expects (but does not guarantee!) that the application will only be invoked this one time during the life of its containing process. Normally, this will only be true for a server based on CGI (or something similar). +The server or the application can store their own data in the +environment, too. The keys must contain at least one dot, +and should be prefixed uniquely. The prefix rack. +is reserved for use with the Rack core distribution and must +not be used otherwise. +The environment must not contain the keys +HTTP_CONTENT_TYPE or HTTP_CONTENT_LENGTH +(use the versions without HTTP_). +The CGI keys (named without a period) must have String values. +There are the following restrictions: +* rack.version must be an array of Integers. +* rack.url_scheme must either be +http+ or +https+. +* There must be a valid input stream in rack.input. +* There must be a valid error stream in rack.errors. +* The REQUEST_METHOD must be a valid token. +* The SCRIPT_NAME, if non-empty, must start with / +* The PATH_INFO, if non-empty, must start with / +* The CONTENT_LENGTH, if given, must consist of digits only. +* One of SCRIPT_NAME or PATH_INFO must be + set. PATH_INFO should be / if + SCRIPT_NAME is empty. + SCRIPT_NAME never should be /, but instead be empty. +=== The Input Stream +The input stream must respond to +gets+, +each+ and +read+. +* +gets+ must be called without arguments and return a string, + or +nil+ on EOF. +* +read+ must be called without or with one integer argument + and return a string, or +nil+ on EOF. +* +each+ must be called without arguments and only yield Strings. +* +close+ must never be called on the input stream. +=== The Error Stream +The error stream must respond to +puts+, +write+ and +flush+. +* +puts+ must be called with a single argument that responds to +to_s+. +* +write+ must be called with a single argument that is a String. +* +flush+ must be called without arguments and must be called + in order to make the error appear for sure. +* +close+ must never be called on the error stream. +== The Response +=== The Status +The status, if parsed as integer (+to_i+), must be greater than or equal to 100. +=== The Headers +The header must respond to each, and yield values of key and value. +The header keys must be Strings. +The header must not contain a +Status+ key, +contain keys with : or newlines in their name, +contain keys names that end in - or _, +but only contain keys that consist of +letters, digits, _ or - and start with a letter. +The values of the header must respond to #each. +The values passed on #each must be Strings +and not contain characters below 037. +=== The Content-Type +There must be a Content-Type, except when the ++Status+ is 1xx, 204 or 304, in which case there must be none +given. +=== The Content-Length +There must be a Content-Length, except when the ++Status+ is 1xx, 204 or 304, in which case there must be none +given. +=== The Body +The Body must respond to #each +and must only yield String values. +If the Body responds to #close, it will be called after iteration. +The Body commonly is an Array of Strings, the application +instance itself, or a File-like object. +== Thanks +Some parts of this specification are adopted from PEP333: Python +Web Server Gateway Interface +v1.0 (http://www.python.org/dev/peps/pep-0333/). I'd like to thank +everyone involved in that effort. diff --git a/vendor/rack-0.9.1/lib/rack.rb b/vendor/rack-0.9.1/lib/rack.rb new file mode 100644 index 00000000..63106383 --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack.rb @@ -0,0 +1,86 @@ +# Copyright (C) 2007, 2008, 2009 Christian Neukirchen +# +# Rack is freely distributable under the terms of an MIT-style license. +# See COPYING or http://www.opensource.org/licenses/mit-license.php. + +$: << File.expand_path(File.dirname(__FILE__)) + + +# The Rack main module, serving as a namespace for all core Rack +# modules and classes. +# +# All modules meant for use in your application are autoloaded here, +# so it should be enough just to require rack.rb in your code. + +module Rack + # The Rack protocol version number implemented. + VERSION = [0,1] + + # Return the Rack protocol version as a dotted string. + def self.version + VERSION.join(".") + end + + # Return the Rack release as a dotted string. + def self.release + "0.9" + end + + autoload :Builder, "rack/builder" + autoload :Cascade, "rack/cascade" + autoload :CommonLogger, "rack/commonlogger" + autoload :ConditionalGet, "rack/conditionalget" + autoload :ContentLength, "rack/content_length" + autoload :File, "rack/file" + autoload :Deflater, "rack/deflater" + autoload :Directory, "rack/directory" + autoload :ForwardRequest, "rack/recursive" + autoload :Handler, "rack/handler" + autoload :Head, "rack/head" + autoload :Lint, "rack/lint" + autoload :MethodOverride, "rack/methodoverride" + autoload :Mime, "rack/mime" + autoload :Recursive, "rack/recursive" + autoload :Reloader, "rack/reloader" + autoload :ShowExceptions, "rack/showexceptions" + autoload :ShowStatus, "rack/showstatus" + autoload :Static, "rack/static" + autoload :URLMap, "rack/urlmap" + autoload :Utils, "rack/utils" + + autoload :MockRequest, "rack/mock" + autoload :MockResponse, "rack/mock" + + autoload :Request, "rack/request" + autoload :Response, "rack/response" + + module Auth + autoload :Basic, "rack/auth/basic" + autoload :AbstractRequest, "rack/auth/abstract/request" + autoload :AbstractHandler, "rack/auth/abstract/handler" + autoload :OpenID, "rack/auth/openid" + module Digest + autoload :MD5, "rack/auth/digest/md5" + autoload :Nonce, "rack/auth/digest/nonce" + autoload :Params, "rack/auth/digest/params" + autoload :Request, "rack/auth/digest/request" + end + end + + module Session + autoload :Cookie, "rack/session/cookie" + autoload :Pool, "rack/session/pool" + autoload :Memcache, "rack/session/memcache" + end + + # *Adapters* connect Rack with third party web frameworks. + # + # Rack includes an adapter for Camping, see README for other + # frameworks supporting Rack in their code bases. + # + # Refer to the submodules for framework-specific calling details. + + module Adapter + autoload :Camping, "rack/adapter/camping" + end +end diff --git a/vendor/rack-0.9.1/lib/rack/adapter/camping.rb b/vendor/rack-0.9.1/lib/rack/adapter/camping.rb new file mode 100644 index 00000000..63bc787f --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/adapter/camping.rb @@ -0,0 +1,22 @@ +module Rack + module Adapter + class Camping + def initialize(app) + @app = app + end + + def call(env) + env["PATH_INFO"] ||= "" + env["SCRIPT_NAME"] ||= "" + controller = @app.run(env['rack.input'], env) + h = controller.headers + h.each_pair do |k,v| + if v.kind_of? URI + h[k] = v.to_s + end + end + [controller.status, controller.headers, [controller.body.to_s]] + end + end + end +end diff --git a/vendor/rack-0.9.1/lib/rack/auth/abstract/handler.rb b/vendor/rack-0.9.1/lib/rack/auth/abstract/handler.rb new file mode 100644 index 00000000..b213eac6 --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/auth/abstract/handler.rb @@ -0,0 +1,28 @@ +module Rack + module Auth + # Rack::Auth::AbstractHandler implements common authentication functionality. + # + # +realm+ should be set for all handlers. + + class AbstractHandler + + attr_accessor :realm + + def initialize(app, &authenticator) + @app, @authenticator = app, authenticator + end + + + private + + def unauthorized(www_authenticate = challenge) + return [ 401, { 'WWW-Authenticate' => www_authenticate.to_s }, [] ] + end + + def bad_request + [ 400, {}, [] ] + end + + end + end +end diff --git a/vendor/rack-0.9.1/lib/rack/auth/abstract/request.rb b/vendor/rack-0.9.1/lib/rack/auth/abstract/request.rb new file mode 100644 index 00000000..1d9ccec6 --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/auth/abstract/request.rb @@ -0,0 +1,37 @@ +module Rack + module Auth + class AbstractRequest + + def initialize(env) + @env = env + end + + def provided? + !authorization_key.nil? + end + + def parts + @parts ||= @env[authorization_key].split(' ', 2) + end + + def scheme + @scheme ||= parts.first.downcase.to_sym + end + + def params + @params ||= parts.last + end + + + private + + AUTHORIZATION_KEYS = ['HTTP_AUTHORIZATION', 'X-HTTP_AUTHORIZATION', 'X_HTTP_AUTHORIZATION'] + + def authorization_key + @authorization_key ||= AUTHORIZATION_KEYS.detect { |key| @env.has_key?(key) } + end + + end + + end +end diff --git a/vendor/rack-0.9.1/lib/rack/auth/basic.rb b/vendor/rack-0.9.1/lib/rack/auth/basic.rb new file mode 100644 index 00000000..95572246 --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/auth/basic.rb @@ -0,0 +1,58 @@ +require 'rack/auth/abstract/handler' +require 'rack/auth/abstract/request' + +module Rack + module Auth + # Rack::Auth::Basic implements HTTP Basic Authentication, as per RFC 2617. + # + # Initialize with the Rack application that you want protecting, + # and a block that checks if a username and password pair are valid. + # + # See also: example/protectedlobster.rb + + class Basic < AbstractHandler + + def call(env) + auth = Basic::Request.new(env) + + return unauthorized unless auth.provided? + + return bad_request unless auth.basic? + + if valid?(auth) + env['REMOTE_USER'] = auth.username + + return @app.call(env) + end + + unauthorized + end + + + private + + def challenge + 'Basic realm="%s"' % realm + end + + def valid?(auth) + @authenticator.call(*auth.credentials) + end + + class Request < Auth::AbstractRequest + def basic? + :basic == scheme + end + + def credentials + @credentials ||= params.unpack("m*").first.split(/:/, 2) + end + + def username + credentials.first + end + end + + end + end +end diff --git a/vendor/rack-0.9.1/lib/rack/auth/digest/md5.rb b/vendor/rack-0.9.1/lib/rack/auth/digest/md5.rb new file mode 100644 index 00000000..6d2bd29c --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/auth/digest/md5.rb @@ -0,0 +1,124 @@ +require 'rack/auth/abstract/handler' +require 'rack/auth/digest/request' +require 'rack/auth/digest/params' +require 'rack/auth/digest/nonce' +require 'digest/md5' + +module Rack + module Auth + module Digest + # Rack::Auth::Digest::MD5 implements the MD5 algorithm version of + # HTTP Digest Authentication, as per RFC 2617. + # + # Initialize with the [Rack] application that you want protecting, + # and a block that looks up a plaintext password for a given username. + # + # +opaque+ needs to be set to a constant base64/hexadecimal string. + # + class MD5 < AbstractHandler + + attr_accessor :opaque + + attr_writer :passwords_hashed + + def initialize(app) + super + @passwords_hashed = nil + end + + def passwords_hashed? + !!@passwords_hashed + end + + def call(env) + auth = Request.new(env) + + unless auth.provided? + return unauthorized + end + + if !auth.digest? || !auth.correct_uri? || !valid_qop?(auth) + return bad_request + end + + if valid?(auth) + if auth.nonce.stale? + return unauthorized(challenge(:stale => true)) + else + env['REMOTE_USER'] = auth.username + + return @app.call(env) + end + end + + unauthorized + end + + + private + + QOP = 'auth'.freeze + + def params(hash = {}) + Params.new do |params| + params['realm'] = realm + params['nonce'] = Nonce.new.to_s + params['opaque'] = H(opaque) + params['qop'] = QOP + + hash.each { |k, v| params[k] = v } + end + end + + def challenge(hash = {}) + "Digest #{params(hash)}" + end + + def valid?(auth) + valid_opaque?(auth) && valid_nonce?(auth) && valid_digest?(auth) + end + + def valid_qop?(auth) + QOP == auth.qop + end + + def valid_opaque?(auth) + H(opaque) == auth.opaque + end + + def valid_nonce?(auth) + auth.nonce.valid? + end + + def valid_digest?(auth) + digest(auth, @authenticator.call(auth.username)) == auth.response + end + + def md5(data) + ::Digest::MD5.hexdigest(data) + end + + alias :H :md5 + + def KD(secret, data) + H([secret, data] * ':') + end + + def A1(auth, password) + [ auth.username, auth.realm, password ] * ':' + end + + def A2(auth) + [ auth.method, auth.uri ] * ':' + end + + def digest(auth, password) + password_hash = passwords_hashed? ? password : H(A1(auth, password)) + + KD(password_hash, [ auth.nonce, auth.nc, auth.cnonce, QOP, H(A2(auth)) ] * ':') + end + + end + end + end +end diff --git a/vendor/rack-0.9.1/lib/rack/auth/digest/nonce.rb b/vendor/rack-0.9.1/lib/rack/auth/digest/nonce.rb new file mode 100644 index 00000000..dbe109f2 --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/auth/digest/nonce.rb @@ -0,0 +1,51 @@ +require 'digest/md5' + +module Rack + module Auth + module Digest + # Rack::Auth::Digest::Nonce is the default nonce generator for the + # Rack::Auth::Digest::MD5 authentication handler. + # + # +private_key+ needs to set to a constant string. + # + # +time_limit+ can be optionally set to an integer (number of seconds), + # to limit the validity of the generated nonces. + + class Nonce + + class << self + attr_accessor :private_key, :time_limit + end + + def self.parse(string) + new(*string.unpack("m*").first.split(' ', 2)) + end + + def initialize(timestamp = Time.now, given_digest = nil) + @timestamp, @given_digest = timestamp.to_i, given_digest + end + + def to_s + [([ @timestamp, digest ] * ' ')].pack("m*").strip + end + + def digest + ::Digest::MD5.hexdigest([ @timestamp, self.class.private_key ] * ':') + end + + def valid? + digest == @given_digest + end + + def stale? + !self.class.time_limit.nil? && (@timestamp - Time.now.to_i) < self.class.time_limit + end + + def fresh? + !stale? + end + + end + end + end +end diff --git a/vendor/rack-0.9.1/lib/rack/auth/digest/params.rb b/vendor/rack-0.9.1/lib/rack/auth/digest/params.rb new file mode 100644 index 00000000..730e2efd --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/auth/digest/params.rb @@ -0,0 +1,55 @@ +module Rack + module Auth + module Digest + class Params < Hash + + def self.parse(str) + split_header_value(str).inject(new) do |header, param| + k, v = param.split('=', 2) + header[k] = dequote(v) + header + end + end + + def self.dequote(str) # From WEBrick::HTTPUtils + ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup + ret.gsub!(/\\(.)/, "\\1") + ret + end + + def self.split_header_value(str) + str.scan( /(\w+\=(?:"[^\"]+"|[^,]+))/n ).collect{ |v| v[0] } + end + + def initialize + super + + yield self if block_given? + end + + def [](k) + super k.to_s + end + + def []=(k, v) + super k.to_s, v.to_s + end + + UNQUOTED = ['qop', 'nc', 'stale'] + + def to_s + inject([]) do |parts, (k, v)| + parts << "#{k}=" + (UNQUOTED.include?(k) ? v.to_s : quote(v)) + parts + end.join(', ') + end + + def quote(str) # From WEBrick::HTTPUtils + '"' << str.gsub(/[\\\"]/o, "\\\1") << '"' + end + + end + end + end +end + diff --git a/vendor/rack-0.9.1/lib/rack/auth/digest/request.rb b/vendor/rack-0.9.1/lib/rack/auth/digest/request.rb new file mode 100644 index 00000000..a0227543 --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/auth/digest/request.rb @@ -0,0 +1,40 @@ +require 'rack/auth/abstract/request' +require 'rack/auth/digest/params' +require 'rack/auth/digest/nonce' + +module Rack + module Auth + module Digest + class Request < Auth::AbstractRequest + + def method + @env['REQUEST_METHOD'] + end + + def digest? + :digest == scheme + end + + def correct_uri? + @env['PATH_INFO'] == uri + end + + def nonce + @nonce ||= Nonce.parse(params['nonce']) + end + + def params + @params ||= Params.parse(parts.last) + end + + def method_missing(sym) + if params.has_key? key = sym.to_s + return params[key] + end + super + end + + end + end + end +end diff --git a/vendor/rack-0.9.1/lib/rack/auth/openid.rb b/vendor/rack-0.9.1/lib/rack/auth/openid.rb new file mode 100644 index 00000000..14eeddd3 --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/auth/openid.rb @@ -0,0 +1,438 @@ +# AUTHOR: blink ; blink#ruby-lang@irc.freenode.net + +gem 'ruby-openid', '~> 2' if defined? Gem +require 'rack/auth/abstract/handler' #rack +require 'uri' #std +require 'pp' #std +require 'openid' #gem +require 'openid/extension' #gem +require 'openid/store/memory' #gem + +module Rack + module Auth + # Rack::Auth::OpenID provides a simple method for permitting + # openid based logins. It requires the ruby-openid library from + # janrain to operate, as well as a rack method of session management. + # + # The ruby-openid home page is at http://openidenabled.com/ruby-openid/. + # + # The OpenID specifications can be found at + # http://openid.net/specs/openid-authentication-1_1.html + # and + # http://openid.net/specs/openid-authentication-2_0.html. Documentation + # for published OpenID extensions and related topics can be found at + # http://openid.net/developers/specs/. + # + # It is recommended to read through the OpenID spec, as well as + # ruby-openid's documentation, to understand what exactly goes on. However + # a setup as simple as the presented examples is enough to provide + # functionality. + # + # This library strongly intends to utilize the OpenID 2.0 features of the + # ruby-openid library, while maintaining OpenID 1.0 compatiblity. + # + # All responses from this rack application will be 303 redirects unless an + # error occurs, with the exception of an authentication request requiring + # an HTML form submission. + # + # NOTE: Extensions are not currently supported by this implimentation of + # the OpenID rack application due to the complexity of the current + # ruby-openid extension handling. + # + # NOTE: Due to the amount of data that this library stores in the + # session, Rack::Session::Cookie may fault. + class OpenID < AbstractHandler + class NoSession < RuntimeError; end + # Required for ruby-openid + OIDStore = ::OpenID::Store::Memory.new + HTML = '%s%s' + + # A Hash of options is taken as it's single initializing + # argument. For example: + # + # simple_oid = OpenID.new('http://mysite.com/') + # + # return_oid = OpenID.new('http://mysite.com/', { + # :return_to => 'http://mysite.com/openid' + # }) + # + # page_oid = OpenID.new('http://mysite.com/', + # :login_good => 'http://mysite.com/auth_good' + # ) + # + # complex_oid = OpenID.new('http://mysite.com/', + # :return_to => 'http://mysite.com/openid', + # :login_good => 'http://mysite.com/user/preferences', + # :auth_fail => [500, {'Content-Type'=>'text/plain'}, + # 'Unable to negotiate with foreign server.'], + # :immediate => true, + # :extensions => { + # ::OpenID::SReg => [['email'],['nickname']] + # } + # ) + # + # = Arguments + # + # The first argument is the realm, identifying the site they are trusting + # with their identity. This is required. + # + # NOTE: In OpenID 1.x, the realm or trust_root is optional and the + # return_to url is required. As this library strives tward ruby-openid + # 2.0, and OpenID 2.0 compatibiliy, the realm is required and return_to + # is optional. However, this implimentation is still backwards compatible + # with OpenID 1.0 servers. + # + # The optional second argument is a hash of options. + # + # == Options + # + # :return_to defines the url to return to after the client + # authenticates with the openid service provider. This url should point + # to where Rack::Auth::OpenID is mounted. If :return_to is not + # provided, :return_to will be the current url including all query + # parameters. + # + # :session_key defines the key to the session hash in the env. + # It defaults to 'rack.session'. + # + # :openid_param defines at what key in the request parameters to + # find the identifier to resolve. As per the 2.0 spec, the default is + # 'openid_identifier'. + # + # :immediate as true will make immediate type of requests the + # default. See OpenID specification documentation. + # + # === URL options + # + # :login_good is the url to go to after the authentication + # process has completed. + # + # :login_fail is the url to go to after the authentication + # process has failed. + # + # :login_quit is the url to go to after the authentication + # process + # has been cancelled. + # + # === Response options + # + # :no_session should be a rack response to be returned if no or + # an incompatible session is found. + # + # :auth_fail should be a rack response to be returned if an + # OpenID::DiscoveryFailure occurs. This is typically due to being unable + # to access the identity url or identity server. + # + # :error should be a rack response to return if any other + # generic error would occur and options[:catch_errors] is true. + # + # === Extensions + # + # :extensions should be a hash of openid extension + # implementations. The key should be the extension main module, the value + # should be an array of arguments for extension::Request.new + # + # The hash is iterated over and passed to #add_extension for processing. + # Please see #add_extension for further documentation. + def initialize(realm, options={}) + @realm = realm + realm = URI(realm) + if realm.path.empty? + raise ArgumentError, "Invalid realm path: '#{realm.path}'" + elsif not realm.absolute? + raise ArgumentError, "Realm '#{@realm}' not absolute" + end + + [:return_to, :login_good, :login_fail, :login_quit].each do |key| + if options.key? key and luri = URI(options[key]) + if !luri.absolute? + raise ArgumentError, ":#{key} is not an absolute uri: '#{luri}'" + end + end + end + + if options[:return_to] and ruri = URI(options[:return_to]) + if ruri.path.empty? + raise ArgumentError, "Invalid return_to path: '#{ruri.path}'" + elsif realm.path != ruri.path[0, realm.path.size] + raise ArgumentError, 'return_to not within realm.' \ + end + end + + # TODO: extension support + if extensions = options.delete(:extensions) + extensions.each do |ext, args| + add_extension ext, *args + end + end + + @options = { + :session_key => 'rack.session', + :openid_param => 'openid_identifier', + #:return_to, :login_good, :login_fail, :login_quit + #:no_session, :auth_fail, :error + :store => OIDStore, + :immediate => false, + :anonymous => false, + :catch_errors => false + }.merge(options) + @extensions = {} + end + + attr_reader :options, :extensions + + # It sets up and uses session data at :openid within the + # session. It sets up the ::OpenID::Consumer using the store specified by + # options[:store]. + # + # If the parameter specified by options[:openid_param] is + # present, processing is passed to #check and the result is returned. + # + # If the parameter 'openid.mode' is set, implying a followup from the + # openid server, processing is passed to #finish and the result is + # returned. + # + # If neither of these conditions are met, a 400 error is returned. + # + # If an error is thrown and options[:catch_errors] is false, the + # exception will be reraised. Otherwise a 500 error is returned. + def call(env) + env['rack.auth.openid'] = self + session = env[@options[:session_key]] + unless session and session.is_a? Hash + raise(NoSession, 'No compatible session') + end + # let us work in our own namespace... + session = (session[:openid] ||= {}) + unless session and session.is_a? Hash + raise(NoSession, 'Incompatible session') + end + + request = Rack::Request.new env + consumer = ::OpenID::Consumer.new session, @options[:store] + + if request.params['openid.mode'] + finish consumer, session, request + elsif request.params[@options[:openid_param]] + check consumer, session, request + else + env['rack.errors'].puts "No valid params provided." + bad_request + end + rescue NoSession + env['rack.errors'].puts($!.message, *$@) + + @options. ### Missing or incompatible session + fetch :no_session, [ 500, + {'Content-Type'=>'text/plain'}, + $!.message ] + rescue + env['rack.errors'].puts($!.message, *$@) + + if not @options[:catch_error] + raise($!) + end + @options. + fetch :error, [ 500, + {'Content-Type'=>'text/plain'}, + 'OpenID has encountered an error.' ] + end + + # As the first part of OpenID consumer action, #check retrieves the data + # required for completion. + # + # * session[:openid][:openid_param] is set to the submitted + # identifier to be authenticated. + # * session[:openid][:site_return] is set as the request's + # HTTP_REFERER, unless already set. + # * env['rack.auth.openid.request'] is the openid checkid + # request instance. + def check(consumer, session, req) + session[:openid_param] = req.params[@options[:openid_param]] + oid = consumer.begin(session[:openid_param], @options[:anonymous]) + pp oid if $DEBUG + req.env['rack.auth.openid.request'] = oid + + session[:site_return] ||= req.env['HTTP_REFERER'] + + # SETUP_NEEDED check! + # see OpenID::Consumer::CheckIDRequest docs + query_args = [@realm, *@options.values_at(:return_to, :immediate)] + query_args[1] ||= req.url + query_args[2] = false if session.key? :setup_needed + pp query_args if $DEBUG + + ## Extension support + extensions.each do |ext,args| + oid.add_extension ext::Request.new(*args) + end + + if oid.send_redirect?(*query_args) + redirect = oid.redirect_url(*query_args) + if $DEBUG + pp redirect + pp Rack::Utils.parse_query(URI(redirect).query) + end + [ 303, {'Location'=>redirect}, [] ] + else + # check on 'action' option. + formbody = oid.form_markup(*query_args) + if $DEBUG + pp formbody + end + body = HTML % ['Confirm...', formbody] + [ 200, {'Content-Type'=>'text/html'}, body.to_a ] + end + rescue ::OpenID::DiscoveryFailure => e + # thrown from inside OpenID::Consumer#begin by yadis stuff + req.env['rack.errors'].puts($!.message, *$@) + + @options. ### Foreign server failed + fetch :auth_fail, [ 503, + {'Content-Type'=>'text/plain'}, + 'Foreign server failure.' ] + end + + # This is the final portion of authentication. Unless any errors outside + # of specification occur, a 303 redirect will be returned with Location + # determined by the OpenID response type. If none of the response type + # :login_* urls are set, the redirect will be set to + # session[:openid][:site_return]. If + # session[:openid][:site_return] is unset, the realm will be + # used. + # + # Any messages from OpenID's response are appended to the 303 response + # body. + # + # Data gathered from extensions are stored in session[:openid] with the + # extension's namespace uri as the key. + # + # * env['rack.auth.openid.response'] is the openid response. + # + # The four valid possible outcomes are: + # * failure: options[:login_fail] or + # session[:site_return] or the realm + # * session[:openid] is cleared and any messages are send to + # rack.errors + # * session[:openid]['authenticated'] is false + # * success: options[:login_good] or + # session[:site_return] or the realm + # * session[:openid] is cleared + # * session[:openid]['authenticated'] is true + # * session[:openid]['identity'] is the actual identifier + # * session[:openid]['identifier'] is the pretty identifier + # * cancel: options[:login_good] or + # session[:site_return] or the realm + # * session[:openid] is cleared + # * session[:openid]['authenticated'] is false + # * setup_needed: resubmits the authentication request. A flag is set for + # non-immediate handling. + # * session[:openid][:setup_needed] is set to true, + # which will prevent immediate style openid authentication. + def finish(consumer, session, req) + oid = consumer.complete(req.params, req.url) + pp oid if $DEBUG + req.env['rack.auth.openid.response'] = oid + + goto = session.fetch :site_return, @realm + body = [] + + case oid.status + when ::OpenID::Consumer::FAILURE + session.clear + session['authenticated'] = false + req.env['rack.errors'].puts oid.message + + goto = @options[:login_fail] if @options.key? :login_fail + body << "Authentication unsuccessful.\n" + when ::OpenID::Consumer::SUCCESS + session.clear + + ## Extension support + extensions.each do |ext, args| + session[ext::NS_URI] = ext::Response. + from_success_response(oid). + get_extension_args + end + + session['authenticated'] = true + # Value for unique identification and such + session['identity'] = oid.identity_url + # Value for display and UI labels + session['identifier'] = oid.display_identifier + + goto = @options[:login_good] if @options.key? :login_good + body << "Authentication successful.\n" + when ::OpenID::Consumer::CANCEL + session.clear + session['authenticated'] = false + + goto = @options[:login_fail] if @options.key? :login_fail + body << "Authentication cancelled.\n" + when ::OpenID::Consumer::SETUP_NEEDED + session[:setup_needed] = true + unless o_id = session[:openid_param] + raise('Required values missing.') + end + + goto = req.script_name+ + '?'+@options[:openid_param]+ + '='+o_id + body << "Reauthentication required.\n" + end + body << oid.message if oid.message + [ 303, {'Location'=>goto}, body] + end + + # The first argument should be the main extension module. + # The extension module should contain the constants: + # * class Request, with OpenID::Extension as an ancestor + # * class Response, with OpenID::Extension as an ancestor + # * string NS_URI, which defines the namespace of the extension, should + # be an absolute http uri + # + # All trailing arguments will be passed to extension::Request.new in + # #check. + # The openid response will be passed to + # extension::Response#from_success_response, #get_extension_args will be + # called on the result to attain the gathered data. + # + # This method returns the key at which the response data will be found in + # the session, which is the namespace uri by default. + def add_extension ext, *args + if not ext.is_a? Module + raise TypeError, "#{ext.inspect} is not a module" + elsif !(m = %w'Request Response NS_URI' - + ext.constants.map{ |c| c.to_s }).empty? + raise ArgumentError, "#{ext.inspect} missing #{m*', '}" + end + + consts = [ext::Request, ext::Response] + + if not consts.all?{|c| c.is_a? Class } + raise TypeError, "#{ext.inspect}'s Request or Response is not a class" + elsif not consts.all?{|c| ::OpenID::Extension > c } + raise ArgumentError, "#{ext.inspect}'s Request or Response not a decendant of OpenID::Extension" + end + + if not ext::NS_URI.is_a? String + raise TypeError, "#{ext.inspect}'s NS_URI is not a string" + elsif not uri = URI(ext::NS_URI) + raise ArgumentError, "#{ext.inspect}'s NS_URI is not a valid uri" + elsif not uri.scheme =~ /^https?$/ + raise ArgumentError, "#{ext.inspect}'s NS_URI is not an http uri" + elsif not uri.absolute? + raise ArgumentError, "#{ext.inspect}'s NS_URI is not and absolute uri" + end + @extensions[ext] = args + return ext::NS_URI + end + + # A conveniance method that returns the namespace of all current + # extensions used by this instance. + def extension_namespaces + @extensions.keys.map{|e|e::NS_URI} + end + end + end +end diff --git a/vendor/rack-0.9.1/lib/rack/builder.rb b/vendor/rack-0.9.1/lib/rack/builder.rb new file mode 100644 index 00000000..25994d5a --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/builder.rb @@ -0,0 +1,67 @@ +module Rack + # Rack::Builder implements a small DSL to iteratively construct Rack + # applications. + # + # Example: + # + # app = Rack::Builder.new { + # use Rack::CommonLogger + # use Rack::ShowExceptions + # map "/lobster" do + # use Rack::Lint + # run Rack::Lobster.new + # end + # } + # + # Or + # + # app = Rack::Builder.app do + # use Rack::CommonLogger + # lambda { |env| [200, {'Content-Type' => 'text/plain'}, 'OK'] } + # end + # + # +use+ adds a middleware to the stack, +run+ dispatches to an application. + # You can use +map+ to construct a Rack::URLMap in a convenient way. + + class Builder + def initialize(&block) + @ins = [] + instance_eval(&block) if block_given? + end + + def self.app(&block) + self.new(&block).to_app + end + + def use(middleware, *args, &block) + @ins << if block_given? + lambda { |app| middleware.new(app, *args, &block) } + else + lambda { |app| middleware.new(app, *args) } + end + end + + def run(app) + @ins << app #lambda { |nothing| app } + end + + def map(path, &block) + if @ins.last.kind_of? Hash + @ins.last[path] = self.class.new(&block).to_app + else + @ins << {} + map(path, &block) + end + end + + def to_app + @ins[-1] = Rack::URLMap.new(@ins.last) if Hash === @ins.last + inner_app = @ins.last + @ins[0...-1].reverse.inject(inner_app) { |a, e| e.call(a) } + end + + def call(env) + to_app.call(env) + end + end +end diff --git a/vendor/rack-0.9.1/lib/rack/cascade.rb b/vendor/rack-0.9.1/lib/rack/cascade.rb new file mode 100644 index 00000000..a038aa11 --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/cascade.rb @@ -0,0 +1,36 @@ +module Rack + # Rack::Cascade tries an request on several apps, and returns the + # first response that is not 404 (or in a list of configurable + # status codes). + + class Cascade + attr_reader :apps + + def initialize(apps, catch=404) + @apps = apps + @catch = [*catch] + end + + def call(env) + status = headers = body = nil + raise ArgumentError, "empty cascade" if @apps.empty? + @apps.each { |app| + begin + status, headers, body = app.call(env) + break unless @catch.include?(status.to_i) + end + } + [status, headers, body] + end + + def add app + @apps << app + end + + def include? app + @apps.include? app + end + + alias_method :<<, :add + end +end diff --git a/vendor/rack-0.9.1/lib/rack/commonlogger.rb b/vendor/rack-0.9.1/lib/rack/commonlogger.rb new file mode 100644 index 00000000..5e68ac62 --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/commonlogger.rb @@ -0,0 +1,61 @@ +module Rack + # Rack::CommonLogger forwards every request to an +app+ given, and + # logs a line in the Apache common log format to the +logger+, or + # rack.errors by default. + + class CommonLogger + def initialize(app, logger=nil) + @app = app + @logger = logger + end + + def call(env) + dup._call(env) + end + + def _call(env) + @env = env + @logger ||= self + @time = Time.now + @status, @header, @body = @app.call(env) + [@status, @header, self] + end + + def close + @body.close if @body.respond_to? :close + end + + # By default, log to rack.errors. + def <<(str) + @env["rack.errors"].write(str) + @env["rack.errors"].flush + end + + def each + length = 0 + @body.each { |part| + length += part.size + yield part + } + + @now = Time.now + + # Common Log Format: http://httpd.apache.org/docs/1.3/logs.html#common + # lilith.local - - [07/Aug/2006 23:58:02] "GET / HTTP/1.1" 500 - + # %{%s - %s [%s] "%s %s%s %s" %d %s\n} % + @logger << %{%s - %s [%s] "%s %s%s %s" %d %s %0.4f\n} % + [ + @env['HTTP_X_FORWARDED_FOR'] || @env["REMOTE_ADDR"] || "-", + @env["REMOTE_USER"] || "-", + @now.strftime("%d/%b/%Y %H:%M:%S"), + @env["REQUEST_METHOD"], + @env["PATH_INFO"], + @env["QUERY_STRING"].empty? ? "" : "?"+@env["QUERY_STRING"], + @env["HTTP_VERSION"], + @status.to_s[0..3], + (length.zero? ? "-" : length.to_s), + @now - @time + ] + end + end +end diff --git a/vendor/rack-0.9.1/lib/rack/conditionalget.rb b/vendor/rack-0.9.1/lib/rack/conditionalget.rb new file mode 100644 index 00000000..e7eb5860 --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/conditionalget.rb @@ -0,0 +1,43 @@ +module Rack + + # Middleware that enables conditional GET using If-None-Match and + # If-Modified-Since. The application should set either or both of the + # Last-Modified or Etag response headers according to RFC 2616. When + # either of the conditions is met, the response body is set to be zero + # length and the response status is set to 304 Not Modified. + # + # Applications that defer response body generation until the body's each + # message is received will avoid response body generation completely when + # a conditional GET matches. + # + # Adapted from Michael Klishin's Merb implementation: + # http://github.com/wycats/merb-core/tree/master/lib/merb-core/rack/middleware/conditional_get.rb + class ConditionalGet + def initialize(app) + @app = app + end + + def call(env) + return @app.call(env) unless %w[GET HEAD].include?(env['REQUEST_METHOD']) + + status, headers, body = @app.call(env) + headers = Utils::HeaderHash.new(headers) + if etag_matches?(env, headers) || modified_since?(env, headers) + status = 304 + body = [] + end + [status, headers, body] + end + + private + def etag_matches?(env, headers) + etag = headers['Etag'] and etag == env['HTTP_IF_NONE_MATCH'] + end + + def modified_since?(env, headers) + last_modified = headers['Last-Modified'] and + last_modified == env['HTTP_IF_MODIFIED_SINCE'] + end + end + +end diff --git a/vendor/rack-0.9.1/lib/rack/content_length.rb b/vendor/rack-0.9.1/lib/rack/content_length.rb new file mode 100644 index 00000000..515a654a --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/content_length.rb @@ -0,0 +1,25 @@ +module Rack + # Sets the Content-Length header on responses with fixed-length bodies. + class ContentLength + def initialize(app) + @app = app + end + + def call(env) + status, headers, body = @app.call(env) + headers = Utils::HeaderHash.new(headers) + + if !Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status) && + !headers['Content-Length'] && + !headers['Transfer-Encoding'] && + (body.respond_to?(:to_ary) || body.respond_to?(:to_str)) + + body = [body] if body.respond_to?(:to_str) # rack 0.4 compat + length = body.to_ary.inject(0) { |len, part| len + part.length } + headers['Content-Length'] = length.to_s + end + + [status, headers, body] + end + end +end diff --git a/vendor/rack-0.9.1/lib/rack/deflater.rb b/vendor/rack-0.9.1/lib/rack/deflater.rb new file mode 100644 index 00000000..7dcc601c --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/deflater.rb @@ -0,0 +1,87 @@ +require "zlib" +require "stringio" +require "time" # for Time.httpdate + +module Rack + +class Deflater + def initialize(app) + @app = app + end + + def call(env) + status, headers, body = @app.call(env) + headers = Utils::HeaderHash.new(headers) + + # Skip compressing empty entity body responses and responses with + # no-transform set. + if Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status) || + headers['Cache-Control'].to_s =~ /\bno-transform\b/ + return [status, headers, body] + end + + request = Request.new(env) + + encoding = Utils.select_best_encoding(%w(gzip deflate identity), + request.accept_encoding) + + # Set the Vary HTTP header. + vary = headers["Vary"].to_s.split(",").map { |v| v.strip } + unless vary.include?("*") || vary.include?("Accept-Encoding") + headers["Vary"] = vary.push("Accept-Encoding").join(",") + end + + case encoding + when "gzip" + mtime = if headers.key?("Last-Modified") + Time.httpdate(headers["Last-Modified"]) + else + Time.now + end + [status, + headers.merge("Content-Encoding" => "gzip"), + self.class.gzip(body, mtime)] + when "deflate" + [status, + headers.merge("Content-Encoding" => "deflate"), + self.class.deflate(body)] + when "identity" + [status, headers, body] + when nil + message = ["An acceptable encoding for the requested resource #{request.fullpath} could not be found."] + [406, {"Content-Type" => "text/plain"}, message] + end + end + + def self.gzip(body, mtime) + io = StringIO.new + gzip = Zlib::GzipWriter.new(io) + gzip.mtime = mtime + + # TODO: Add streaming + body.each { |part| gzip << part } + + gzip.close + return io.string + end + + DEFLATE_ARGS = [ + Zlib::DEFAULT_COMPRESSION, + # drop the zlib header which causes both Safari and IE to choke + -Zlib::MAX_WBITS, + Zlib::DEF_MEM_LEVEL, + Zlib::DEFAULT_STRATEGY + ] + + # Loosely based on Mongrel's Deflate handler + def self.deflate(body) + deflater = Zlib::Deflate.new(*DEFLATE_ARGS) + + # TODO: Add streaming + body.each { |part| deflater << part } + + return deflater.finish + end +end + +end diff --git a/vendor/rack-0.9.1/lib/rack/directory.rb b/vendor/rack-0.9.1/lib/rack/directory.rb new file mode 100644 index 00000000..570edd48 --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/directory.rb @@ -0,0 +1,150 @@ +require 'time' +require 'rack/mime' + +module Rack + # Rack::Directory serves entries below the +root+ given, according to the + # path info of the Rack request. If a directory is found, the file's contents + # will be presented in an html based index. If a file is found, the env will + # be passed to the specified +app+. + # + # If +app+ is not specified, a Rack::File of the same +root+ will be used. + + class Directory + DIR_FILE = "%s%s%s%s" + DIR_PAGE = <<-PAGE + + %s + + + +

%s

+
+ + + + + + + +%s +
NameSizeTypeLast Modified
+
+ + PAGE + + attr_reader :files + attr_accessor :root, :path + + def initialize(root, app=nil) + @root = F.expand_path(root) + @app = app || Rack::File.new(@root) + end + + def call(env) + dup._call(env) + end + + F = ::File + + def _call(env) + @env = env + @script_name = env['SCRIPT_NAME'] + @path_info = Utils.unescape(env['PATH_INFO']) + + if forbidden = check_forbidden + forbidden + else + @path = F.join(@root, @path_info) + list_path + end + end + + def check_forbidden + return unless @path_info.include? ".." + + body = "Forbidden\n" + size = body.respond_to?(:bytesize) ? body.bytesize : body.size + return [403, {"Content-Type" => "text/plain","Content-Length" => size.to_s}, [body]] + end + + def list_directory + @files = [['../','Parent Directory','','','']] + glob = F.join(@path, '*') + + Dir[glob].sort.each do |node| + stat = stat(node) + next unless stat + basename = F.basename(node) + ext = F.extname(node) + + url = F.join(@script_name, @path_info, basename) + size = stat.size + type = stat.directory? ? 'directory' : Mime.mime_type(ext) + size = stat.directory? ? '-' : filesize_format(size) + mtime = stat.mtime.httpdate + + @files << [ url, basename, size, type, mtime ] + end + + return [ 200, {'Content-Type'=>'text/html; charset=utf-8'}, self ] + end + + def stat(node, max = 10) + F.stat(node) + rescue Errno::ENOENT, Errno::ELOOP + return nil + end + + # TODO: add correct response if not readable, not sure if 404 is the best + # option + def list_path + @stat = F.stat(@path) + + if @stat.readable? + return @app.call(@env) if @stat.file? + return list_directory if @stat.directory? + else + raise Errno::ENOENT, 'No such file or directory' + end + + rescue Errno::ENOENT, Errno::ELOOP + return entity_not_found + end + + def entity_not_found + body = "Entity not found: #{@path_info}\n" + size = body.respond_to?(:bytesize) ? body.bytesize : body.size + return [404, {"Content-Type" => "text/plain", "Content-Length" => size.to_s}, [body]] + end + + def each + show_path = @path.sub(/^#{@root}/,'') + files = @files.map{|f| DIR_FILE % f }*"\n" + page = DIR_PAGE % [ show_path, show_path , files ] + page.each_line{|l| yield l } + end + + # Stolen from Ramaze + + FILESIZE_FORMAT = [ + ['%.1fT', 1 << 40], + ['%.1fG', 1 << 30], + ['%.1fM', 1 << 20], + ['%.1fK', 1 << 10], + ] + + def filesize_format(int) + FILESIZE_FORMAT.each do |format, size| + return format % (int.to_f / size) if int >= size + end + + int.to_s + 'B' + end + end +end diff --git a/vendor/rack-0.9.1/lib/rack/file.rb b/vendor/rack-0.9.1/lib/rack/file.rb new file mode 100644 index 00000000..4bc6198d --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/file.rb @@ -0,0 +1,85 @@ +require 'time' +require 'rack/mime' + +module Rack + # Rack::File serves files below the +root+ given, according to the + # path info of the Rack request. + # + # Handlers can detect if bodies are a Rack::File, and use mechanisms + # like sendfile on the +path+. + + class File + attr_accessor :root + attr_accessor :path + + def initialize(root) + @root = root + end + + def call(env) + dup._call(env) + end + + F = ::File + + def _call(env) + @path_info = Utils.unescape(env["PATH_INFO"]) + return forbidden if @path_info.include? ".." + + @path = F.join(@root, @path_info) + + begin + if F.file?(@path) && F.readable?(@path) + serving + else + raise Errno::EPERM + end + rescue SystemCallError + not_found + end + end + + def forbidden + body = "Forbidden\n" + [403, {"Content-Type" => "text/plain", + "Content-Length" => body.size.to_s}, + [body]] + end + + # NOTE: + # We check via File::size? whether this file provides size info + # via stat (e.g. /proc files often don't), otherwise we have to + # figure it out by reading the whole file into memory. And while + # we're at it we also use this as body then. + + def serving + if size = F.size?(@path) + body = self + else + body = [F.read(@path)] + size = body.first.size + end + + [200, { + "Last-Modified" => F.mtime(@path).httpdate, + "Content-Type" => Mime.mime_type(F.extname(@path), 'text/plain'), + "Content-Length" => size.to_s + }, body] + end + + def not_found + body = "File not found: #{@path_info}\n" + [404, {"Content-Type" => "text/plain", + "Content-Length" => body.size.to_s}, + [body]] + end + + def each + F.open(@path, "rb") { |file| + while part = file.read(8192) + yield part + end + } + end + end +end diff --git a/vendor/rack-0.9.1/lib/rack/handler.rb b/vendor/rack-0.9.1/lib/rack/handler.rb new file mode 100644 index 00000000..1018af64 --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/handler.rb @@ -0,0 +1,48 @@ +module Rack + # *Handlers* connect web servers with Rack. + # + # Rack includes Handlers for Mongrel, WEBrick, FastCGI, CGI, SCGI + # and LiteSpeed. + # + # Handlers usually are activated by calling MyHandler.run(myapp). + # A second optional hash can be passed to include server-specific + # configuration. + module Handler + def self.get(server) + return unless server + + if klass = @handlers[server] + obj = Object + klass.split("::").each { |x| obj = obj.const_get(x) } + obj + else + Rack::Handler.const_get(server.capitalize) + end + end + + def self.register(server, klass) + @handlers ||= {} + @handlers[server] = klass + end + + autoload :CGI, "rack/handler/cgi" + autoload :FastCGI, "rack/handler/fastcgi" + autoload :Mongrel, "rack/handler/mongrel" + autoload :EventedMongrel, "rack/handler/evented_mongrel" + autoload :SwiftipliedMongrel, "rack/handler/swiftiplied_mongrel" + autoload :WEBrick, "rack/handler/webrick" + autoload :LSWS, "rack/handler/lsws" + autoload :SCGI, "rack/handler/scgi" + autoload :Thin, "rack/handler/thin" + + register 'cgi', 'Rack::Handler::CGI' + register 'fastcgi', 'Rack::Handler::FastCGI' + register 'mongrel', 'Rack::Handler::Mongrel' + register 'emongrel', 'Rack::Handler::EventedMongrel' + register 'smongrel', 'Rack::Handler::SwiftipliedMongrel' + register 'webrick', 'Rack::Handler::WEBrick' + register 'lsws', 'Rack::Handler::LSWS' + register 'scgi', 'Rack::Handler::SCGI' + register 'thin', 'Rack::Handler::Thin' + end +end diff --git a/vendor/rack-0.9.1/lib/rack/handler/cgi.rb b/vendor/rack-0.9.1/lib/rack/handler/cgi.rb new file mode 100644 index 00000000..1922402c --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/handler/cgi.rb @@ -0,0 +1,57 @@ +module Rack + module Handler + class CGI + def self.run(app, options=nil) + serve app + end + + def self.serve(app) + env = ENV.to_hash + env.delete "HTTP_CONTENT_LENGTH" + + env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/" + + env.update({"rack.version" => [0,1], + "rack.input" => STDIN, + "rack.errors" => STDERR, + + "rack.multithread" => false, + "rack.multiprocess" => true, + "rack.run_once" => true, + + "rack.url_scheme" => ["yes", "on", "1"].include?(ENV["HTTPS"]) ? "https" : "http" + }) + + env["QUERY_STRING"] ||= "" + env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"] + env["REQUEST_PATH"] ||= "/" + + status, headers, body = app.call(env) + begin + send_headers status, headers + send_body body + ensure + body.close if body.respond_to? :close + end + end + + def self.send_headers(status, headers) + STDOUT.print "Status: #{status}\r\n" + headers.each { |k, vs| + vs.each { |v| + STDOUT.print "#{k}: #{v}\r\n" + } + } + STDOUT.print "\r\n" + STDOUT.flush + end + + def self.send_body(body) + body.each { |part| + STDOUT.print part + STDOUT.flush + } + end + end + end +end diff --git a/vendor/rack-0.9.1/lib/rack/handler/evented_mongrel.rb b/vendor/rack-0.9.1/lib/rack/handler/evented_mongrel.rb new file mode 100644 index 00000000..0f5cbf72 --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/handler/evented_mongrel.rb @@ -0,0 +1,8 @@ +require 'swiftcore/evented_mongrel' + +module Rack + module Handler + class EventedMongrel < Handler::Mongrel + end + end +end diff --git a/vendor/rack-0.9.1/lib/rack/handler/fastcgi.rb b/vendor/rack-0.9.1/lib/rack/handler/fastcgi.rb new file mode 100644 index 00000000..75b94e99 --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/handler/fastcgi.rb @@ -0,0 +1,86 @@ +require 'fcgi' +require 'socket' + +module Rack + module Handler + class FastCGI + def self.run(app, options={}) + file = options[:File] and STDIN.reopen(UNIXServer.new(file)) + port = options[:Port] and STDIN.reopen(TCPServer.new(port)) + FCGI.each { |request| + serve request, app + } + end + + module ProperStream # :nodoc: + def each # This is missing by default. + while line = gets + yield line + end + end + + def read(*args) + if args.empty? + super || "" # Empty string on EOF. + else + super + end + end + end + + def self.serve(request, app) + env = request.env + env.delete "HTTP_CONTENT_LENGTH" + + request.in.extend ProperStream + + env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/" + + env.update({"rack.version" => [0,1], + "rack.input" => request.in, + "rack.errors" => request.err, + + "rack.multithread" => false, + "rack.multiprocess" => true, + "rack.run_once" => 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 "PATH_INFO" if env["PATH_INFO"] == "" + env.delete "CONTENT_TYPE" if env["CONTENT_TYPE"] == "" + env.delete "CONTENT_LENGTH" if env["CONTENT_LENGTH"] == "" + + 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 + request.finish + end + end + + def self.send_headers(out, status, headers) + out.print "Status: #{status}\r\n" + headers.each { |k, vs| + vs.each { |v| + out.print "#{k}: #{v}\r\n" + } + } + out.print "\r\n" + out.flush + end + + def self.send_body(out, body) + body.each { |part| + out.print part + out.flush + } + end + end + end +end diff --git a/vendor/rack-0.9.1/lib/rack/handler/lsws.rb b/vendor/rack-0.9.1/lib/rack/handler/lsws.rb new file mode 100644 index 00000000..48b82b58 --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/handler/lsws.rb @@ -0,0 +1,52 @@ +require 'lsapi' +#require 'cgi' +module Rack + module Handler + class LSWS + def self.run(app, options=nil) + while LSAPI.accept != nil + serve app + end + end + def self.serve(app) + env = ENV.to_hash + env.delete "HTTP_CONTENT_LENGTH" + env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/" + env.update({"rack.version" => [0,1], + "rack.input" => STDIN, + "rack.errors" => STDERR, + "rack.multithread" => false, + "rack.multiprocess" => true, + "rack.run_once" => false, + "rack.url_scheme" => ["yes", "on", "1"].include?(ENV["HTTPS"]) ? "https" : "http" + }) + env["QUERY_STRING"] ||= "" + env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"] + env["REQUEST_PATH"] ||= "/" + status, headers, body = app.call(env) + begin + send_headers status, headers + send_body body + ensure + body.close if body.respond_to? :close + end + end + def self.send_headers(status, headers) + print "Status: #{status}\r\n" + headers.each { |k, vs| + vs.each { |v| + print "#{k}: #{v}\r\n" + } + } + print "\r\n" + STDOUT.flush + end + def self.send_body(body) + body.each { |part| + print part + STDOUT.flush + } + end + end + end +end diff --git a/vendor/rack-0.9.1/lib/rack/handler/mongrel.rb b/vendor/rack-0.9.1/lib/rack/handler/mongrel.rb new file mode 100644 index 00000000..5673598b --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/handler/mongrel.rb @@ -0,0 +1,82 @@ +require 'mongrel' +require 'stringio' + +module Rack + module Handler + class Mongrel < ::Mongrel::HttpHandler + def self.run(app, options={}) + server = ::Mongrel::HttpServer.new(options[:Host] || '0.0.0.0', + options[:Port] || 8080) + # 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 + + def initialize(app) + @app = app + end + + def process(request, response) + env = {}.replace(request.params) + env.delete "HTTP_CONTENT_TYPE" + env.delete "HTTP_CONTENT_LENGTH" + + env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/" + + env.update({"rack.version" => [0,1], + "rack.input" => request.body || StringIO.new(""), + "rack.errors" => STDERR, + + "rack.multithread" => true, + "rack.multiprocess" => false, # ??? + "rack.run_once" => false, + + "rack.url_scheme" => "http", + }) + env["QUERY_STRING"] ||= "" + env.delete "PATH_INFO" if env["PATH_INFO"] == "" + + status, headers, body = @app.call(env) + + begin + response.status = status.to_i + response.send_status(nil) + + headers.each { |k, vs| + vs.each { |v| + response.header[k] = v + } + } + response.send_header + + body.each { |part| + response.write part + response.socket.flush + } + ensure + body.close if body.respond_to? :close + end + end + end + end +end diff --git a/vendor/rack-0.9.1/lib/rack/handler/scgi.rb b/vendor/rack-0.9.1/lib/rack/handler/scgi.rb new file mode 100644 index 00000000..0e143395 --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/handler/scgi.rb @@ -0,0 +1,57 @@ +require 'scgi' +require 'stringio' + +module Rack + module Handler + class SCGI < ::SCGI::Processor + attr_accessor :app + + def self.run(app, options=nil) + new(options.merge(:app=>app, + :host=>options[:Host], + :port=>options[:Port], + :socket=>options[:Socket])).listen + end + + def initialize(settings = {}) + @app = settings[:app] + @log = Object.new + def @log.info(*args); end + def @log.error(*args); end + super(settings) + end + + def process_request(request, input_body, socket) + env = {}.replace(request) + env.delete "HTTP_CONTENT_TYPE" + env.delete "HTTP_CONTENT_LENGTH" + env["REQUEST_PATH"], env["QUERY_STRING"] = env["REQUEST_URI"].split('?', 2) + env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"] + env["PATH_INFO"] = env["REQUEST_PATH"] + env["QUERY_STRING"] ||= "" + env["SCRIPT_NAME"] = "" + env.update({"rack.version" => [0,1], + "rack.input" => StringIO.new(input_body), + "rack.errors" => STDERR, + + "rack.multithread" => true, + "rack.multiprocess" => true, + "rack.run_once" => false, + + "rack.url_scheme" => ["yes", "on", "1"].include?(env["HTTPS"]) ? "https" : "http" + }) + status, headers, body = app.call(env) + begin + socket.write("Status: #{status}\r\n") + headers.each do |k, vs| + vs.each {|v| socket.write("#{k}: #{v}\r\n")} + end + socket.write("\r\n") + body.each {|s| socket.write(s)} + ensure + body.close if body.respond_to? :close + end + end + end + end +end diff --git a/vendor/rack-0.9.1/lib/rack/handler/swiftiplied_mongrel.rb b/vendor/rack-0.9.1/lib/rack/handler/swiftiplied_mongrel.rb new file mode 100644 index 00000000..4bafd0b9 --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/handler/swiftiplied_mongrel.rb @@ -0,0 +1,8 @@ +require 'swiftcore/swiftiplied_mongrel' + +module Rack + module Handler + class SwiftipliedMongrel < Handler::Mongrel + end + end +end diff --git a/vendor/rack-0.9.1/lib/rack/handler/thin.rb b/vendor/rack-0.9.1/lib/rack/handler/thin.rb new file mode 100644 index 00000000..7ad088b3 --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/handler/thin.rb @@ -0,0 +1,15 @@ +require "thin" + +module Rack + module Handler + class Thin + def self.run(app, options={}) + server = ::Thin::Server.new(options[:Host] || '0.0.0.0', + options[:Port] || 8080, + app) + yield server if block_given? + server.start + end + end + end +end diff --git a/vendor/rack-0.9.1/lib/rack/handler/webrick.rb b/vendor/rack-0.9.1/lib/rack/handler/webrick.rb new file mode 100644 index 00000000..9674af80 --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/handler/webrick.rb @@ -0,0 +1,61 @@ +require 'webrick' +require 'stringio' + +module Rack + module Handler + class WEBrick < ::WEBrick::HTTPServlet::AbstractServlet + def self.run(app, options={}) + server = ::WEBrick::HTTPServer.new(options) + server.mount "/", Rack::Handler::WEBrick, app + trap(:INT) { server.shutdown } + yield server if block_given? + server.start + end + + def initialize(server, app) + super server + @app = app + end + + def service(req, res) + env = req.meta_vars + env.delete_if { |k, v| v.nil? } + + env.update({"rack.version" => [0,1], + "rack.input" => StringIO.new(req.body.to_s), + "rack.errors" => STDERR, + + "rack.multithread" => true, + "rack.multiprocess" => false, + "rack.run_once" => false, + + "rack.url_scheme" => ["yes", "on", "1"].include?(ENV["HTTPS"]) ? "https" : "http" + }) + + env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"] + env["QUERY_STRING"] ||= "" + env["REQUEST_PATH"] ||= "/" + env.delete "PATH_INFO" if env["PATH_INFO"] == "" + + status, headers, body = @app.call(env) + begin + res.status = status.to_i + headers.each { |k, vs| + if k.downcase == "set-cookie" + res.cookies.concat vs.to_a + else + vs.each { |v| + res[k] = v + } + end + } + body.each { |part| + res.body << part + } + ensure + body.close if body.respond_to? :close + end + end + end + end +end diff --git a/vendor/rack-0.9.1/lib/rack/head.rb b/vendor/rack-0.9.1/lib/rack/head.rb new file mode 100644 index 00000000..deab822a --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/head.rb @@ -0,0 +1,19 @@ +module Rack + +class Head + def initialize(app) + @app = app + end + + def call(env) + status, headers, body = @app.call(env) + + if env["REQUEST_METHOD"] == "HEAD" + [status, headers, []] + else + [status, headers, body] + end + end +end + +end diff --git a/vendor/rack-0.9.1/lib/rack/lint.rb b/vendor/rack-0.9.1/lib/rack/lint.rb new file mode 100644 index 00000000..e7f805f1 --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/lint.rb @@ -0,0 +1,465 @@ +module Rack + # Rack::Lint validates your application and the requests and + # responses according to the Rack spec. + + class Lint + def initialize(app) + @app = app + end + + # :stopdoc: + + class LintError < RuntimeError; end + module Assertion + def assert(message, &block) + unless block.call + raise LintError, message + end + end + end + include Assertion + + ## This specification aims to formalize the Rack protocol. You + ## can (and should) use Rack::Lint to enforce it. + ## + ## When you develop middleware, be sure to add a Lint before and + ## after to catch all mistakes. + + ## = Rack applications + + ## A Rack application is an Ruby object (not a class) that + ## responds to +call+. + def call(env=nil) + dup._call(env) + end + + def _call(env) + ## It takes exactly one argument, the *environment* + assert("No env given") { env } + check_env env + + env['rack.input'] = InputWrapper.new(env['rack.input']) + env['rack.errors'] = ErrorWrapper.new(env['rack.errors']) + + ## and returns an Array of exactly three values: + status, headers, @body = @app.call(env) + ## The *status*, + check_status status + ## the *headers*, + check_headers headers + ## and the *body*. + check_content_type status, headers + check_content_length status, headers, env + [status, headers, self] + end + + ## == The Environment + def check_env(env) + ## The environment must be an true instance of Hash (no + ## subclassing allowed) that includes CGI-like headers. + ## The application is free to modify the environment. + assert("env #{env.inspect} is not a Hash, but #{env.class}") { + env.instance_of? Hash + } + + ## + ## The environment is required to include these variables + ## (adopted from PEP333), except when they'd be empty, but see + ## below. + + ## REQUEST_METHOD:: The HTTP request method, such as + ## "GET" or "POST". This cannot ever + ## be an empty string, and so is + ## always required. + + ## SCRIPT_NAME:: The initial portion of the request + ## URL's "path" that corresponds to the + ## application object, so that the + ## application knows its virtual + ## "location". This may be an empty + ## string, if the application corresponds + ## to the "root" of the server. + + ## PATH_INFO:: The remainder of the request URL's + ## "path", designating the virtual + ## "location" of the request's target + ## within the application. This may be an + ## empty string, if the request URL targets + ## the application root and does not have a + ## trailing slash. + + ## QUERY_STRING:: The portion of the request URL that + ## follows the ?, if any. May be + ## empty, but is always required! + + ## SERVER_NAME, SERVER_PORT:: When combined with SCRIPT_NAME and PATH_INFO, these variables can be used to complete the URL. Note, however, that HTTP_HOST, if present, should be used in preference to SERVER_NAME for reconstructing the request URL. SERVER_NAME and SERVER_PORT can never be empty strings, and so are always required. + + ## HTTP_ Variables:: Variables corresponding to the + ## client-supplied HTTP request + ## headers (i.e., variables whose + ## names begin with HTTP_). The + ## presence or absence of these + ## variables should correspond with + ## the presence or absence of the + ## appropriate HTTP header in the + ## request. + + ## In addition to this, the Rack environment must include these + ## Rack-specific variables: + + ## rack.version:: The Array [0,1], representing this version of Rack. + ## rack.url_scheme:: +http+ or +https+, depending on the request URL. + ## rack.input:: See below, the input stream. + ## rack.errors:: See below, the error stream. + ## rack.multithread:: true if the application object may be simultaneously invoked by another thread in the same process, false otherwise. + ## rack.multiprocess:: true if an equivalent application object may be simultaneously invoked by another process, false otherwise. + ## rack.run_once:: true if the server expects (but does not guarantee!) that the application will only be invoked this one time during the life of its containing process. Normally, this will only be true for a server based on CGI (or something similar). + + ## The server or the application can store their own data in the + ## environment, too. The keys must contain at least one dot, + ## and should be prefixed uniquely. The prefix rack. + ## is reserved for use with the Rack core distribution and must + ## not be used otherwise. + ## + + %w[REQUEST_METHOD SERVER_NAME SERVER_PORT + QUERY_STRING + rack.version rack.input rack.errors + rack.multithread rack.multiprocess rack.run_once].each { |header| + assert("env missing required key #{header}") { env.include? header } + } + + ## The environment must not contain the keys + ## HTTP_CONTENT_TYPE or HTTP_CONTENT_LENGTH + ## (use the versions without HTTP_). + %w[HTTP_CONTENT_TYPE HTTP_CONTENT_LENGTH].each { |header| + assert("env contains #{header}, must use #{header[5,-1]}") { + not env.include? header + } + } + + ## The CGI keys (named without a period) must have String values. + env.each { |key, value| + next if key.include? "." # Skip extensions + assert("env variable #{key} has non-string value #{value.inspect}") { + value.instance_of? String + } + } + + ## + ## There are the following restrictions: + + ## * rack.version must be an array of Integers. + assert("rack.version must be an Array, was #{env["rack.version"].class}") { + env["rack.version"].instance_of? Array + } + ## * rack.url_scheme must either be +http+ or +https+. + assert("rack.url_scheme unknown: #{env["rack.url_scheme"].inspect}") { + %w[http https].include? env["rack.url_scheme"] + } + + ## * There must be a valid input stream in rack.input. + check_input env["rack.input"] + ## * There must be a valid error stream in rack.errors. + check_error env["rack.errors"] + + ## * The REQUEST_METHOD must be a valid token. + assert("REQUEST_METHOD unknown: #{env["REQUEST_METHOD"]}") { + env["REQUEST_METHOD"] =~ /\A[0-9A-Za-z!\#$%&'*+.^_`|~-]+\z/ + } + + ## * The SCRIPT_NAME, if non-empty, must start with / + assert("SCRIPT_NAME must start with /") { + !env.include?("SCRIPT_NAME") || + env["SCRIPT_NAME"] == "" || + env["SCRIPT_NAME"] =~ /\A\// + } + ## * The PATH_INFO, if non-empty, must start with / + assert("PATH_INFO must start with /") { + !env.include?("PATH_INFO") || + env["PATH_INFO"] == "" || + env["PATH_INFO"] =~ /\A\// + } + ## * The CONTENT_LENGTH, if given, must consist of digits only. + assert("Invalid CONTENT_LENGTH: #{env["CONTENT_LENGTH"]}") { + !env.include?("CONTENT_LENGTH") || env["CONTENT_LENGTH"] =~ /\A\d+\z/ + } + + ## * One of SCRIPT_NAME or PATH_INFO must be + ## set. PATH_INFO should be / if + ## SCRIPT_NAME is empty. + assert("One of SCRIPT_NAME or PATH_INFO must be set (make PATH_INFO '/' if SCRIPT_NAME is empty)") { + env["SCRIPT_NAME"] || env["PATH_INFO"] + } + ## SCRIPT_NAME never should be /, but instead be empty. + assert("SCRIPT_NAME cannot be '/', make it '' and PATH_INFO '/'") { + env["SCRIPT_NAME"] != "/" + } + end + + ## === The Input Stream + def check_input(input) + ## The input stream must respond to +gets+, +each+ and +read+. + [:gets, :each, :read].each { |method| + assert("rack.input #{input} does not respond to ##{method}") { + input.respond_to? method + } + } + end + + class InputWrapper + include Assertion + + def initialize(input) + @input = input + end + + def size + @input.size + end + + def rewind + @input.rewind + end + + ## * +gets+ must be called without arguments and return a string, + ## or +nil+ on EOF. + def gets(*args) + assert("rack.input#gets called with arguments") { args.size == 0 } + v = @input.gets + assert("rack.input#gets didn't return a String") { + v.nil? or v.instance_of? String + } + v + end + + ## * +read+ must be called without or with one integer argument + ## and return a string, or +nil+ on EOF. + def read(*args) + assert("rack.input#read called with too many arguments") { + args.size <= 1 + } + if args.size == 1 + assert("rack.input#read called with non-integer argument") { + args.first.kind_of? Integer + } + end + v = @input.read(*args) + assert("rack.input#read didn't return a String") { + v.nil? or v.instance_of? String + } + v + end + + ## * +each+ must be called without arguments and only yield Strings. + def each(*args) + assert("rack.input#each called with arguments") { args.size == 0 } + @input.each { |line| + assert("rack.input#each didn't yield a String") { + line.instance_of? String + } + yield line + } + end + + ## * +close+ must never be called on the input stream. + def close(*args) + assert("rack.input#close must not be called") { false } + end + end + + ## === The Error Stream + def check_error(error) + ## The error stream must respond to +puts+, +write+ and +flush+. + [:puts, :write, :flush].each { |method| + assert("rack.error #{error} does not respond to ##{method}") { + error.respond_to? method + } + } + end + + class ErrorWrapper + include Assertion + + def initialize(error) + @error = error + end + + ## * +puts+ must be called with a single argument that responds to +to_s+. + def puts(str) + @error.puts str + end + + ## * +write+ must be called with a single argument that is a String. + def write(str) + assert("rack.errors#write not called with a String") { str.instance_of? String } + @error.write str + end + + ## * +flush+ must be called without arguments and must be called + ## in order to make the error appear for sure. + def flush + @error.flush + end + + ## * +close+ must never be called on the error stream. + def close(*args) + assert("rack.errors#close must not be called") { false } + end + end + + ## == The Response + + ## === The Status + def check_status(status) + ## The status, if parsed as integer (+to_i+), must be greater than or equal to 100. + assert("Status must be >=100 seen as integer") { status.to_i >= 100 } + end + + ## === The Headers + def check_headers(header) + ## The header must respond to each, and yield values of key and value. + assert("headers object should respond to #each, but doesn't (got #{header.class} as headers)") { + header.respond_to? :each + } + header.each { |key, value| + ## The header keys must be Strings. + assert("header key must be a string, was #{key.class}") { + key.instance_of? String + } + ## The header must not contain a +Status+ key, + assert("header must not contain Status") { key.downcase != "status" } + ## contain keys with : or newlines in their name, + assert("header names must not contain : or \\n") { key !~ /[:\n]/ } + ## contain keys names that end in - or _, + assert("header names must not end in - or _") { key !~ /[-_]\z/ } + ## but only contain keys that consist of + ## letters, digits, _ or - and start with a letter. + assert("invalid header name: #{key}") { key =~ /\A[a-zA-Z][a-zA-Z0-9_-]*\z/ } + ## + ## The values of the header must respond to #each. + assert("header values must respond to #each, but the value of " + + "'#{key}' doesn't (is #{value.class})") { value.respond_to? :each } + value.each { |item| + ## The values passed on #each must be Strings + assert("header values must consist of Strings, but '#{key}' also contains a #{item.class}") { + item.instance_of?(String) + } + ## and not contain characters below 037. + assert("invalid header value #{key}: #{item.inspect}") { + item !~ /[\000-\037]/ + } + } + } + end + + ## === The Content-Type + def check_content_type(status, headers) + headers.each { |key, value| + ## There must be a Content-Type, except when the + ## +Status+ is 1xx, 204 or 304, in which case there must be none + ## given. + if key.downcase == "content-type" + assert("Content-Type header found in #{status} response, not allowed") { + not Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include? status.to_i + } + return + end + } + assert("No Content-Type header found") { + Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include? status.to_i + } + end + + ## === The Content-Length + def check_content_length(status, headers, env) + chunked_response = false + headers.each { |key, value| + if key.downcase == 'transfer-encoding' + chunked_response = value.downcase != 'identity' + end + } + + headers.each { |key, value| + if key.downcase == 'content-length' + ## There must be a Content-Length, except when the + ## +Status+ is 1xx, 204 or 304, in which case there must be none + ## given. + assert("Content-Length header found in #{status} response, not allowed") { + not Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include? status.to_i + } + + assert('Content-Length header should not be used if body is chunked') { + not chunked_response + } + + bytes = 0 + string_body = true + + @body.each { |part| + unless part.kind_of?(String) + string_body = false + break + end + + bytes += (part.respond_to?(:bytesize) ? part.bytesize : part.size) + } + + if env["REQUEST_METHOD"] == "HEAD" + assert("Response body was given for HEAD request, but should be empty") { + bytes == 0 + } + else + if string_body + assert("Content-Length header was #{value}, but should be #{bytes}") { + value == bytes.to_s + } + end + end + + return + end + } + + if [ String, Array ].include?(@body.class) && !chunked_response + assert('No Content-Length header found') { + Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include? status.to_i + } + end + end + + ## === The Body + def each + @closed = false + ## The Body must respond to #each + @body.each { |part| + ## and must only yield String values. + assert("Body yielded non-string value #{part.inspect}") { + part.instance_of? String + } + yield part + } + ## + ## If the Body responds to #close, it will be called after iteration. + # XXX howto: assert("Body has not been closed") { @closed } + + ## + ## The Body commonly is an Array of Strings, the application + ## instance itself, or a File-like object. + end + + def close + @closed = true + @body.close if @body.respond_to?(:close) + end + + # :startdoc: + + end +end + +## == Thanks +## Some parts of this specification are adopted from PEP333: Python +## Web Server Gateway Interface +## v1.0 (http://www.python.org/dev/peps/pep-0333/). I'd like to thank +## everyone involved in that effort. diff --git a/vendor/rack-0.9.1/lib/rack/lobster.rb b/vendor/rack-0.9.1/lib/rack/lobster.rb new file mode 100644 index 00000000..f63f419a --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/lobster.rb @@ -0,0 +1,65 @@ +require 'zlib' + +require 'rack/request' +require 'rack/response' + +module Rack + # Paste has a Pony, Rack has a Lobster! + class Lobster + LobsterString = Zlib::Inflate.inflate("eJx9kEEOwyAMBO99xd7MAcytUhPlJyj2 + P6jy9i4k9EQyGAnBarEXeCBqSkntNXsi/ZCvC48zGQoZKikGrFMZvgS5ZHd+aGWVuWwhVF0 + t1drVmiR42HcWNz5w3QanT+2gIvTVCiE1lm1Y0eU4JGmIIbaKwextKn8rvW+p5PIwFl8ZWJ + I8jyiTlhTcYXkekJAzTyYN6E08A+dk8voBkAVTJQ==".delete("\n ").unpack("m*")[0]) + + LambdaLobster = lambda { |env| + if env["QUERY_STRING"].include?("flip") + lobster = LobsterString.split("\n"). + map { |line| line.ljust(42).reverse }. + join("\n") + href = "?" + else + lobster = LobsterString + href = "?flip" + end + + content = ["Lobstericious!", + "
", lobster, "
", + "flip!"] + length = content.inject(0) { |a,e| a+e.size }.to_s + [200, {"Content-Type" => "text/html", "Content-Length" => length}, content] + } + + def call(env) + req = Request.new(env) + if req.GET["flip"] == "left" + lobster = LobsterString.split("\n"). + map { |line| line.ljust(42).reverse }. + join("\n") + href = "?flip=right" + elsif req.GET["flip"] == "crash" + raise "Lobster crashed" + else + lobster = LobsterString + href = "?flip=left" + end + + res = Response.new + res.write "Lobstericious!" + res.write "
"
+      res.write lobster
+      res.write "
" + res.write "

flip!

" + res.write "

crash!

" + res.finish + end + + end +end + +if $0 == __FILE__ + require 'rack' + require 'rack/showexceptions' + Rack::Handler::WEBrick.run \ + Rack::ShowExceptions.new(Rack::Lint.new(Rack::Lobster.new)), + :Port => 9292 +end diff --git a/vendor/rack-0.9.1/lib/rack/methodoverride.rb b/vendor/rack-0.9.1/lib/rack/methodoverride.rb new file mode 100644 index 00000000..0eed29f4 --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/methodoverride.rb @@ -0,0 +1,27 @@ +module Rack + class MethodOverride + HTTP_METHODS = %w(GET HEAD PUT POST DELETE OPTIONS) + + METHOD_OVERRIDE_PARAM_KEY = "_method".freeze + HTTP_METHOD_OVERRIDE_HEADER = "HTTP_X_HTTP_METHOD_OVERRIDE".freeze + + def initialize(app) + @app = app + end + + def call(env) + if env["REQUEST_METHOD"] == "POST" + req = Request.new(env) + method = req.POST[METHOD_OVERRIDE_PARAM_KEY] || + env[HTTP_METHOD_OVERRIDE_HEADER] + method = method.to_s.upcase + if HTTP_METHODS.include?(method) + env["rack.methodoverride.original_method"] = env["REQUEST_METHOD"] + env["REQUEST_METHOD"] = method + end + end + + @app.call(env) + end + end +end diff --git a/vendor/rack-0.9.1/lib/rack/mime.rb b/vendor/rack-0.9.1/lib/rack/mime.rb new file mode 100644 index 00000000..2e325670 --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/mime.rb @@ -0,0 +1,204 @@ +module Rack + module Mime + # Returns String with mime type if found, otherwise use +fallback+. + # +ext+ should be filename extension in the '.ext' format that + # File.extname(file) returns. + # +fallback+ may be any object + # + # Also see the documentation for MIME_TYPES + # + # Usage: + # Rack::Utils.mime_type('.foo') + # + # This is a shortcut for: + # Rack::Utils::MIME_TYPES.fetch('.foo', 'application/octet-stream') + + def mime_type(ext, fallback='application/octet-stream') + MIME_TYPES.fetch(ext, fallback) + end + module_function :mime_type + + # List of most common mime-types, selected various sources + # according to their usefulness in a webserving scope for Ruby + # users. + # + # To amend this list with your local mime.types list you can use: + # + # require 'webrick/httputils' + # list = WEBrick::HTTPUtils.load_mime_types('/etc/mime.types') + # Rack::Utils::MIME_TYPES.merge!(list) + # + # To add the list mongrel provides, use: + # + # require 'mongrel/handlers' + # Rack::Utils::MIME_TYPES.merge!(Mongrel::DirHandler::MIME_TYPES) + + MIME_TYPES = { + ".3gp" => "video/3gpp", + ".a" => "application/octet-stream", + ".ai" => "application/postscript", + ".aif" => "audio/x-aiff", + ".aiff" => "audio/x-aiff", + ".asc" => "application/pgp-signature", + ".asf" => "video/x-ms-asf", + ".asm" => "text/x-asm", + ".asx" => "video/x-ms-asf", + ".atom" => "application/atom+xml", + ".au" => "audio/basic", + ".avi" => "video/x-msvideo", + ".bat" => "application/x-msdownload", + ".bin" => "application/octet-stream", + ".bmp" => "image/bmp", + ".bz2" => "application/x-bzip2", + ".c" => "text/x-c", + ".cab" => "application/vnd.ms-cab-compressed", + ".cc" => "text/x-c", + ".chm" => "application/vnd.ms-htmlhelp", + ".class" => "application/octet-stream", + ".com" => "application/x-msdownload", + ".conf" => "text/plain", + ".cpp" => "text/x-c", + ".crt" => "application/x-x509-ca-cert", + ".css" => "text/css", + ".csv" => "text/csv", + ".cxx" => "text/x-c", + ".deb" => "application/x-debian-package", + ".der" => "application/x-x509-ca-cert", + ".diff" => "text/x-diff", + ".djv" => "image/vnd.djvu", + ".djvu" => "image/vnd.djvu", + ".dll" => "application/x-msdownload", + ".dmg" => "application/octet-stream", + ".doc" => "application/msword", + ".dot" => "application/msword", + ".dtd" => "application/xml-dtd", + ".dvi" => "application/x-dvi", + ".ear" => "application/java-archive", + ".eml" => "message/rfc822", + ".eps" => "application/postscript", + ".exe" => "application/x-msdownload", + ".f" => "text/x-fortran", + ".f77" => "text/x-fortran", + ".f90" => "text/x-fortran", + ".flv" => "video/x-flv", + ".for" => "text/x-fortran", + ".gem" => "application/octet-stream", + ".gemspec" => "text/x-script.ruby", + ".gif" => "image/gif", + ".gz" => "application/x-gzip", + ".h" => "text/x-c", + ".hh" => "text/x-c", + ".htm" => "text/html", + ".html" => "text/html", + ".ico" => "image/vnd.microsoft.icon", + ".ics" => "text/calendar", + ".ifb" => "text/calendar", + ".iso" => "application/octet-stream", + ".jar" => "application/java-archive", + ".java" => "text/x-java-source", + ".jnlp" => "application/x-java-jnlp-file", + ".jpeg" => "image/jpeg", + ".jpg" => "image/jpeg", + ".js" => "application/javascript", + ".json" => "application/json", + ".log" => "text/plain", + ".m3u" => "audio/x-mpegurl", + ".m4v" => "video/mp4", + ".man" => "text/troff", + ".mathml" => "application/mathml+xml", + ".mbox" => "application/mbox", + ".mdoc" => "text/troff", + ".me" => "text/troff", + ".mid" => "audio/midi", + ".midi" => "audio/midi", + ".mime" => "message/rfc822", + ".mml" => "application/mathml+xml", + ".mng" => "video/x-mng", + ".mov" => "video/quicktime", + ".mp3" => "audio/mpeg", + ".mp4" => "video/mp4", + ".mp4v" => "video/mp4", + ".mpeg" => "video/mpeg", + ".mpg" => "video/mpeg", + ".ms" => "text/troff", + ".msi" => "application/x-msdownload", + ".odp" => "application/vnd.oasis.opendocument.presentation", + ".ods" => "application/vnd.oasis.opendocument.spreadsheet", + ".odt" => "application/vnd.oasis.opendocument.text", + ".ogg" => "application/ogg", + ".p" => "text/x-pascal", + ".pas" => "text/x-pascal", + ".pbm" => "image/x-portable-bitmap", + ".pdf" => "application/pdf", + ".pem" => "application/x-x509-ca-cert", + ".pgm" => "image/x-portable-graymap", + ".pgp" => "application/pgp-encrypted", + ".pkg" => "application/octet-stream", + ".pl" => "text/x-script.perl", + ".pm" => "text/x-script.perl-module", + ".png" => "image/png", + ".pnm" => "image/x-portable-anymap", + ".ppm" => "image/x-portable-pixmap", + ".pps" => "application/vnd.ms-powerpoint", + ".ppt" => "application/vnd.ms-powerpoint", + ".ps" => "application/postscript", + ".psd" => "image/vnd.adobe.photoshop", + ".py" => "text/x-script.python", + ".qt" => "video/quicktime", + ".ra" => "audio/x-pn-realaudio", + ".rake" => "text/x-script.ruby", + ".ram" => "audio/x-pn-realaudio", + ".rar" => "application/x-rar-compressed", + ".rb" => "text/x-script.ruby", + ".rdf" => "application/rdf+xml", + ".roff" => "text/troff", + ".rpm" => "application/x-redhat-package-manager", + ".rss" => "application/rss+xml", + ".rtf" => "application/rtf", + ".ru" => "text/x-script.ruby", + ".s" => "text/x-asm", + ".sgm" => "text/sgml", + ".sgml" => "text/sgml", + ".sh" => "application/x-sh", + ".sig" => "application/pgp-signature", + ".snd" => "audio/basic", + ".so" => "application/octet-stream", + ".svg" => "image/svg+xml", + ".svgz" => "image/svg+xml", + ".swf" => "application/x-shockwave-flash", + ".t" => "text/troff", + ".tar" => "application/x-tar", + ".tbz" => "application/x-bzip-compressed-tar", + ".tcl" => "application/x-tcl", + ".tex" => "application/x-tex", + ".texi" => "application/x-texinfo", + ".texinfo" => "application/x-texinfo", + ".text" => "text/plain", + ".tif" => "image/tiff", + ".tiff" => "image/tiff", + ".torrent" => "application/x-bittorrent", + ".tr" => "text/troff", + ".txt" => "text/plain", + ".vcf" => "text/x-vcard", + ".vcs" => "text/x-vcalendar", + ".vrml" => "model/vrml", + ".war" => "application/java-archive", + ".wav" => "audio/x-wav", + ".wma" => "audio/x-ms-wma", + ".wmv" => "video/x-ms-wmv", + ".wmx" => "video/x-ms-wmx", + ".wrl" => "model/vrml", + ".wsdl" => "application/wsdl+xml", + ".xbm" => "image/x-xbitmap", + ".xhtml" => "application/xhtml+xml", + ".xls" => "application/vnd.ms-excel", + ".xml" => "application/xml", + ".xpm" => "image/x-xpixmap", + ".xsl" => "application/xml", + ".xslt" => "application/xslt+xml", + ".yaml" => "text/yaml", + ".yml" => "text/yaml", + ".zip" => "application/zip", + } + end +end diff --git a/vendor/rack-0.9.1/lib/rack/mock.rb b/vendor/rack-0.9.1/lib/rack/mock.rb new file mode 100644 index 00000000..f43b9af3 --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/mock.rb @@ -0,0 +1,160 @@ +require 'uri' +require 'stringio' +require 'rack/lint' +require 'rack/utils' +require 'rack/response' + +module Rack + # Rack::MockRequest helps testing your Rack application without + # actually using HTTP. + # + # After performing a request on a URL with get/post/put/delete, it + # returns a MockResponse with useful helper methods for effective + # testing. + # + # You can pass a hash with additional configuration to the + # get/post/put/delete. + # :input:: A String or IO-like to be used as rack.input. + # :fatal:: Raise a FatalWarning if the app writes to rack.errors. + # :lint:: If true, wrap the application in a Rack::Lint. + + class MockRequest + class FatalWarning < RuntimeError + end + + class FatalWarner + def puts(warning) + raise FatalWarning, warning + end + + def write(warning) + raise FatalWarning, warning + end + + def flush + end + + def string + "" + end + end + + DEFAULT_ENV = { + "rack.version" => [0,1], + "rack.input" => StringIO.new, + "rack.errors" => StringIO.new, + "rack.multithread" => true, + "rack.multiprocess" => true, + "rack.run_once" => false, + } + + def initialize(app) + @app = app + end + + def get(uri, opts={}) request("GET", uri, opts) end + def post(uri, opts={}) request("POST", uri, opts) end + def put(uri, opts={}) request("PUT", uri, opts) end + def delete(uri, opts={}) request("DELETE", uri, opts) end + + def request(method="GET", uri="", opts={}) + env = self.class.env_for(uri, opts.merge(:method => method)) + + if opts[:lint] + app = Rack::Lint.new(@app) + else + app = @app + end + + errors = env["rack.errors"] + MockResponse.new(*(app.call(env) + [errors])) + end + + # Return the Rack environment used for a request to +uri+. + def self.env_for(uri="", opts={}) + uri = URI(uri) + env = DEFAULT_ENV.dup + + env["REQUEST_METHOD"] = opts[:method] || "GET" + env["SERVER_NAME"] = uri.host || "example.org" + env["SERVER_PORT"] = uri.port ? uri.port.to_s : "80" + env["QUERY_STRING"] = uri.query.to_s + env["PATH_INFO"] = (!uri.path || uri.path.empty?) ? "/" : uri.path + env["rack.url_scheme"] = uri.scheme || "http" + + env["SCRIPT_NAME"] = opts[:script_name] || "" + + if opts[:fatal] + env["rack.errors"] = FatalWarner.new + else + env["rack.errors"] = StringIO.new + end + + opts[:input] ||= "" + if String === opts[:input] + env["rack.input"] = StringIO.new(opts[:input]) + else + env["rack.input"] = opts[:input] + end + + opts.each { |field, value| + env[field] = value if String === field + } + + env + end + end + + # Rack::MockResponse provides useful helpers for testing your apps. + # Usually, you don't create the MockResponse on your own, but use + # MockRequest. + + class MockResponse + def initialize(status, headers, body, errors=StringIO.new("")) + @status = status.to_i + + @original_headers = headers + @headers = Rack::Utils::HeaderHash.new + headers.each { |field, values| + values.each { |value| + @headers[field] = value + } + @headers[field] = "" if values.empty? + } + + @body = "" + body.each { |part| @body << part } + + @errors = errors.string + end + + # Status + attr_reader :status + + # Headers + attr_reader :headers, :original_headers + + def [](field) + headers[field] + end + + + # Body + attr_reader :body + + def =~(other) + @body =~ other + end + + def match(other) + @body.match other + end + + + # Errors + attr_accessor :errors + + + include Response::Helpers + end +end diff --git a/vendor/rack-0.9.1/lib/rack/recursive.rb b/vendor/rack-0.9.1/lib/rack/recursive.rb new file mode 100644 index 00000000..bf8b9659 --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/recursive.rb @@ -0,0 +1,57 @@ +require 'uri' + +module Rack + # Rack::ForwardRequest gets caught by Rack::Recursive and redirects + # the current request to the app at +url+. + # + # raise ForwardRequest.new("/not-found") + # + + class ForwardRequest < Exception + attr_reader :url, :env + + def initialize(url, env={}) + @url = URI(url) + @env = env + + @env["PATH_INFO"] = @url.path + @env["QUERY_STRING"] = @url.query if @url.query + @env["HTTP_HOST"] = @url.host if @url.host + @env["HTTP_PORT"] = @url.port if @url.port + @env["rack.url_scheme"] = @url.scheme if @url.scheme + + super "forwarding to #{url}" + end + end + + # Rack::Recursive allows applications called down the chain to + # include data from other applications (by using + # rack['rack.recursive.include'][...] or raise a + # ForwardRequest to redirect internally. + + class Recursive + def initialize(app) + @app = app + end + + def call(env) + @script_name = env["SCRIPT_NAME"] + @app.call(env.merge('rack.recursive.include' => method(:include))) + rescue ForwardRequest => req + call(env.merge(req.env)) + end + + def include(env, path) + unless path.index(@script_name) == 0 && (path[@script_name.size] == ?/ || + path[@script_name.size].nil?) + raise ArgumentError, "can only include below #{@script_name}, not #{path}" + end + + env = env.merge("PATH_INFO" => path, "SCRIPT_NAME" => @script_name, + "REQUEST_METHOD" => "GET", + "CONTENT_LENGTH" => "0", "CONTENT_TYPE" => "", + "rack.input" => StringIO.new("")) + @app.call(env) + end + end +end diff --git a/vendor/rack-0.9.1/lib/rack/reloader.rb b/vendor/rack-0.9.1/lib/rack/reloader.rb new file mode 100644 index 00000000..25ca2f9e --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/reloader.rb @@ -0,0 +1,64 @@ +require 'thread' + +module Rack + # Rack::Reloader checks on every request, but at most every +secs+ + # seconds, if a file loaded changed, and reloads it, logging to + # rack.errors. + # + # It is recommended you use ShowExceptions to catch SyntaxErrors etc. + + class Reloader + def initialize(app, secs=10) + @app = app + @secs = secs # reload every @secs seconds max + @last = Time.now + end + + def call(env) + if Time.now > @last + @secs + Thread.exclusive { + reload!(env['rack.errors']) + @last = Time.now + } + end + + @app.call(env) + end + + def reload!(stderr=STDERR) + need_reload = $LOADED_FEATURES.find_all { |loaded| + begin + if loaded =~ /\A[.\/]/ # absolute filename or 1.9 + abs = loaded + else + abs = $LOAD_PATH.map { |path| ::File.join(path, loaded) }. + find { |file| ::File.exist? file } + end + + if abs + ::File.mtime(abs) > @last - @secs rescue false + else + false + end + end + } + + need_reload.each { |l| + $LOADED_FEATURES.delete l + } + + need_reload.each { |to_load| + begin + if require to_load + stderr.puts "#{self.class}: reloaded `#{to_load}'" + end + rescue LoadError, SyntaxError => e + raise e # Possibly ShowExceptions + end + } + + stderr.flush + need_reload + end + end +end diff --git a/vendor/rack-0.9.1/lib/rack/request.rb b/vendor/rack-0.9.1/lib/rack/request.rb new file mode 100644 index 00000000..08021d0c --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/request.rb @@ -0,0 +1,218 @@ +require 'rack/utils' + +module Rack + # Rack::Request provides a convenient interface to a Rack + # environment. It is stateless, the environment +env+ passed to the + # constructor will be directly modified. + # + # req = Rack::Request.new(env) + # req.post? + # req.params["data"] + + class Request + # The environment of the request. + attr_reader :env + + def initialize(env) + @env = env + end + + def body; @env["rack.input"] end + def scheme; @env["rack.url_scheme"] end + def script_name; @env["SCRIPT_NAME"].to_s end + def path_info; @env["PATH_INFO"].to_s end + def port; @env["SERVER_PORT"].to_i end + def request_method; @env["REQUEST_METHOD"] end + def query_string; @env["QUERY_STRING"].to_s end + def content_length; @env['CONTENT_LENGTH'] end + def content_type; @env['CONTENT_TYPE'] end + + # The media type (type/subtype) portion of the CONTENT_TYPE header + # without any media type parameters. e.g., when CONTENT_TYPE is + # "text/plain;charset=utf-8", the media-type is "text/plain". + # + # For more information on the use of media types in HTTP, see: + # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7 + def media_type + content_type && content_type.split(/\s*[;,]\s*/, 2)[0].downcase + end + + # The media type parameters provided in CONTENT_TYPE as a Hash, or + # an empty Hash if no CONTENT_TYPE or media-type parameters were + # provided. e.g., when the CONTENT_TYPE is "text/plain;charset=utf-8", + # this method responds with the following Hash: + # { 'charset' => 'utf-8' } + def media_type_params + return {} if content_type.nil? + content_type.split(/\s*[;,]\s*/)[1..-1]. + collect { |s| s.split('=', 2) }. + inject({}) { |hash,(k,v)| hash[k.downcase] = v ; hash } + end + + # The character set of the request body if a "charset" media type + # parameter was given, or nil if no "charset" was specified. Note + # that, per RFC2616, text/* media types that specify no explicit + # charset are to be considered ISO-8859-1. + def content_charset + media_type_params['charset'] + end + + def host + # Remove port number. + (@env["HTTP_HOST"] || @env["SERVER_NAME"]).gsub(/:\d+\z/, '') + end + + def script_name=(s); @env["SCRIPT_NAME"] = s.to_s end + def path_info=(s); @env["PATH_INFO"] = s.to_s end + + def get?; request_method == "GET" end + def post?; request_method == "POST" end + def put?; request_method == "PUT" end + def delete?; request_method == "DELETE" end + def head?; request_method == "HEAD" end + + # The set of form-data media-types. Requests that do not indicate + # one of the media types presents in this list will not be eligible + # for form-data / param parsing. + FORM_DATA_MEDIA_TYPES = [ + nil, + 'application/x-www-form-urlencoded', + 'multipart/form-data' + ] + + # Determine whether the request body contains form-data by checking + # the request media_type against registered form-data media-types: + # "application/x-www-form-urlencoded" and "multipart/form-data". The + # list of form-data media types can be modified through the + # +FORM_DATA_MEDIA_TYPES+ array. + def form_data? + FORM_DATA_MEDIA_TYPES.include?(media_type) + end + + # Returns the data recieved in the query string. + def GET + if @env["rack.request.query_string"] == query_string + @env["rack.request.query_hash"] + else + @env["rack.request.query_string"] = query_string + @env["rack.request.query_hash"] = + Utils.parse_query(query_string) + end + end + + # Returns the data recieved in the request body. + # + # This method support both application/x-www-form-urlencoded and + # multipart/form-data. + def POST + if @env["rack.request.form_input"].eql? @env["rack.input"] + @env["rack.request.form_hash"] + elsif form_data? + @env["rack.request.form_input"] = @env["rack.input"] + unless @env["rack.request.form_hash"] = + Utils::Multipart.parse_multipart(env) + @env["rack.request.form_vars"] = @env["rack.input"].read + @env["rack.request.form_hash"] = Utils.parse_query(@env["rack.request.form_vars"]) + @env["rack.input"].rewind if @env["rack.input"].respond_to?(:rewind) + end + @env["rack.request.form_hash"] + else + {} + end + end + + # The union of GET and POST data. + def params + self.put? ? self.GET : self.GET.update(self.POST) + rescue EOFError => e + self.GET + end + + # shortcut for request.params[key] + def [](key) + params[key.to_s] + end + + # shortcut for request.params[key] = value + def []=(key, value) + params[key.to_s] = value + end + + # like Hash#values_at + def values_at(*keys) + keys.map{|key| params[key] } + end + + # the referer of the client or '/' + def referer + @env['HTTP_REFERER'] || '/' + end + alias referrer referer + + + def cookies + return {} unless @env["HTTP_COOKIE"] + + if @env["rack.request.cookie_string"] == @env["HTTP_COOKIE"] + @env["rack.request.cookie_hash"] + else + @env["rack.request.cookie_string"] = @env["HTTP_COOKIE"] + # According to RFC 2109: + # If multiple cookies satisfy the criteria above, they are ordered in + # the Cookie header such that those with more specific Path attributes + # precede those with less specific. Ordering with respect to other + # attributes (e.g., Domain) is unspecified. + @env["rack.request.cookie_hash"] = + Utils.parse_query(@env["rack.request.cookie_string"], ';,').inject({}) {|h,(k,v)| + h[k] = Array === v ? v.first : v + h + } + end + end + + def xhr? + @env["HTTP_X_REQUESTED_WITH"] == "XMLHttpRequest" + end + + # Tries to return a remake of the original request URL as a string. + def url + url = scheme + "://" + url << host + + if scheme == "https" && port != 443 || + scheme == "http" && port != 80 + url << ":#{port}" + end + + url << fullpath + + url + end + + def fullpath + path = script_name + path_info + path << "?" << query_string unless query_string.empty? + path + end + + def accept_encoding + @env["HTTP_ACCEPT_ENCODING"].to_s.split(/,\s*/).map do |part| + m = /^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$/.match(part) # From WEBrick + + if m + [m[1], (m[2] || 1.0).to_f] + else + raise "Invalid value for Accept-Encoding: #{part.inspect}" + end + end + end + + def ip + if addr = @env['HTTP_X_FORWARDED_FOR'] + addr.split(',').last.strip + else + @env['REMOTE_ADDR'] + end + end + end +end diff --git a/vendor/rack-0.9.1/lib/rack/response.rb b/vendor/rack-0.9.1/lib/rack/response.rb new file mode 100644 index 00000000..97deb6ef --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/response.rb @@ -0,0 +1,171 @@ +require 'rack/request' +require 'rack/utils' + +module Rack + # Rack::Response provides a convenient interface to create a Rack + # response. + # + # It allows setting of headers and cookies, and provides useful + # defaults (a OK response containing HTML). + # + # You can use Response#write to iteratively generate your response, + # but note that this is buffered by Rack::Response until you call + # +finish+. +finish+ however can take a block inside which calls to + # +write+ are syncronous with the Rack response. + # + # Your application's +call+ should end returning Response#finish. + + class Response + def initialize(body=[], status=200, header={}, &block) + @status = status + @header = Utils::HeaderHash.new({"Content-Type" => "text/html"}. + merge(header)) + + @writer = lambda { |x| @body << x } + @block = nil + @length = 0 + + @body = [] + + if body.respond_to? :to_str + write body.to_str + elsif body.respond_to?(:each) + body.each { |part| + write part.to_s + } + else + raise TypeError, "stringable or iterable required" + end + + yield self if block_given? + end + + attr_reader :header + attr_accessor :status, :body + + def [](key) + header[key] + end + + def []=(key, value) + header[key] = value + end + + def set_cookie(key, value) + case value + when Hash + domain = "; domain=" + value[:domain] if value[:domain] + path = "; path=" + value[:path] if value[:path] + # According to RFC 2109, we need dashes here. + # N.B.: cgi.rb uses spaces... + expires = "; expires=" + value[:expires].clone.gmtime. + strftime("%a, %d-%b-%Y %H:%M:%S GMT") if value[:expires] + secure = "; secure" if value[:secure] + value = value[:value] + end + value = [value] unless Array === value + cookie = Utils.escape(key) + "=" + + value.map { |v| Utils.escape v }.join("&") + + "#{domain}#{path}#{expires}#{secure}" + + case self["Set-Cookie"] + when Array + self["Set-Cookie"] << cookie + when String + self["Set-Cookie"] = [self["Set-Cookie"], cookie] + when nil + self["Set-Cookie"] = cookie + end + end + + def delete_cookie(key, value={}) + unless Array === self["Set-Cookie"] + self["Set-Cookie"] = [self["Set-Cookie"]].compact + end + + self["Set-Cookie"].reject! { |cookie| + cookie =~ /\A#{Utils.escape(key)}=/ + } + + set_cookie(key, + {:value => '', :path => nil, :domain => nil, + :expires => Time.at(0) }.merge(value)) + end + + + def finish(&block) + @block = block + + if [204, 304].include?(status.to_i) + header.delete "Content-Type" + [status.to_i, header.to_hash, []] + else + header["Content-Length"] ||= @length.to_s + [status.to_i, header.to_hash, self] + end + end + alias to_a finish # For *response + + def each(&callback) + @body.each(&callback) + @writer = callback + @block.call(self) if @block + end + + def write(str) + s = str.to_s + @length += s.size + @writer.call s + str + end + + def close + body.close if body.respond_to?(:close) + end + + def empty? + @block == nil && @body.empty? + end + + alias headers header + + module Helpers + def invalid?; @status < 100 || @status >= 600; end + + def informational?; @status >= 100 && @status < 200; end + def successful?; @status >= 200 && @status < 300; end + def redirection?; @status >= 300 && @status < 400; end + def client_error?; @status >= 400 && @status < 500; end + def server_error?; @status >= 500 && @status < 600; end + + def ok?; @status == 200; end + def forbidden?; @status == 403; end + def not_found?; @status == 404; end + + def redirect?; [301, 302, 303, 307].include? @status; end + def empty?; [201, 204, 304].include? @status; end + + # Headers + attr_reader :headers, :original_headers + + def include?(header) + !!headers[header] + end + + def content_type + headers["Content-Type"] + end + + def content_length + cl = headers["Content-Length"] + cl ? cl.to_i : cl + end + + def location + headers["Location"] + end + end + + include Helpers + end +end diff --git a/vendor/rack-0.9.1/lib/rack/session/abstract/id.rb b/vendor/rack-0.9.1/lib/rack/session/abstract/id.rb new file mode 100644 index 00000000..c521ba16 --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/session/abstract/id.rb @@ -0,0 +1,153 @@ +# AUTHOR: blink ; blink#ruby-lang@irc.freenode.net +# bugrep: Andreas Zehnder + +require 'rack/utils' +require 'time' + +module Rack + module Session + module Abstract + # ID sets up a basic framework for implementing an id based sessioning + # service. Cookies sent to the client for maintaining sessions will only + # contain an id reference. Only #get_session and #set_session should + # need to be overwritten. + # + # All parameters are optional. + # * :key determines the name of the cookie, by default it is + # 'rack.session' + # * :domain and :path set the related cookie values, by default + # domain is nil, and the path is '/'. + # * :expire_after is the number of seconds in which the session + # cookie will expire. By default it is set not to provide any + # expiry time. + class ID + attr_reader :key + DEFAULT_OPTIONS = { + :key => 'rack.session', + :path => '/', + :domain => nil, + :expire_after => nil, + :secure => false, + :httponly => true, + :sidbits => 128 + } + + def initialize(app, options={}) + @default_options = self.class::DEFAULT_OPTIONS.merge(options) + @key = @default_options[:key] + @default_context = context app + end + + def call(env) + @default_context.call(env) + end + + def context(app) + Rack::Utils::Context.new self, app do |env| + load_session env + response = app.call(env) + commit_session env, response + response + end + end + + private + + # Generate a new session id using Ruby #rand. The size of the + # session id is controlled by the :sidbits option. + # Monkey patch this to use custom methods for session id generation. + def generate_sid + "%0#{@default_options[:sidbits] / 4}x" % + rand(2**@default_options[:sidbits] - 1) + end + + # Extracts the session id from provided cookies and passes it and the + # environment to #get_session. It then sets the resulting session into + # 'rack.session', and places options and session metadata into + # 'rack.session.options'. + def load_session(env) + sid = (env['HTTP_COOKIE']||'')[/#{@key}=([^,;]+)/,1] + sid, session = get_session(env, sid) + unless session.is_a?(Hash) + puts 'Session: '+sid.inspect+"\n"+session.inspect if $DEBUG + raise TypeError, 'Session not a Hash' + end + + options = @default_options. + merge({ :id => sid, :by => self, :at => Time.now }) + + env['rack.session'] = session + env['rack.session.options'] = options + + return true + end + + # Acquires the session from the environment and the session id from + # the session options and passes them to #set_session. It then + # proceeds to set a cookie up in the response with the session's id. + def commit_session(env, response) + unless response.is_a?(Array) + puts 'Response: '+response.inspect if $DEBUG + raise ArgumentError, 'Response is not an array.' + end + + options = env['rack.session.options'] + unless options.is_a?(Hash) + puts 'Options: '+options.inspect if $DEBUG + raise TypeError, 'Options not a Hash' + end + + sid, time, z = options.values_at(:id, :at, :by) + unless self == z + warn "#{self} not managing this session." + return + end + + unless env['rack.session'].is_a?(Hash) + warn 'Session: '+sid.inspect+"\n"+session.inspect if $DEBUG + raise TypeError, 'Session not a Hash' + end + + unless set_session(env, sid) + warn "Session not saved." if $DEBUG + warn "#{env['rack.session'].inspect} has been lost."if $DEBUG + return false + end + + cookie = Utils.escape(@key)+'='+Utils.escape(sid) + cookie<< "; domain=#{options[:domain]}" if options[:domain] + cookie<< "; path=#{options[:path]}" if options[:path] + if options[:expire_after] + expiry = time + options[:expire_after] + cookie<< "; expires=#{expiry.httpdate}" + end + cookie<< "; Secure" if options[:secure] + cookie<< "; HttpOnly" if options[:httponly] + + case a = (h = response[1])['Set-Cookie'] + when Array then a << cookie + when String then h['Set-Cookie'] = [a, cookie] + when nil then h['Set-Cookie'] = cookie + end + + return true + end + + # Should return [session_id, session]. All thread safety and session + # retrival proceedures should occur here. + # If nil is provided as the session id, generation of a new valid id + # should occur within. + def get_session(env, sid) + raise '#get_session needs to be implemented.' + end + + # All thread safety and session storage proceedures should occur here. + # Should return true or false dependant on whether or not the session + # was saved or not. + def set_session(env, sid) + raise '#set_session needs to be implemented.' + end + end + end + end +end diff --git a/vendor/rack-0.9.1/lib/rack/session/cookie.rb b/vendor/rack-0.9.1/lib/rack/session/cookie.rb new file mode 100644 index 00000000..3dba358c --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/session/cookie.rb @@ -0,0 +1,89 @@ +require 'openssl' + +module Rack + + module Session + + # Rack::Session::Cookie provides simple cookie based session management. + # The session is a Ruby Hash stored as base64 encoded marshalled data + # set to :key (default: rack.session). + # When the secret key is set, cookie data is checked for data integrity. + # + # Example: + # + # use Rack::Session::Cookie, :key => 'rack.session', + # :domain => 'foo.com', + # :path => '/', + # :expire_after => 2592000, + # :secret => 'change_me' + # + # All parameters are optional. + + class Cookie + + def initialize(app, options={}) + @app = app + @key = options[:key] || "rack.session" + @secret = options[:secret] + @default_options = {:domain => nil, + :path => "/", + :expire_after => nil}.merge(options) + end + + def call(env) + load_session(env) + status, headers, body = @app.call(env) + commit_session(env, status, headers, body) + end + + private + + def load_session(env) + request = Rack::Request.new(env) + session_data = request.cookies[@key] + + if @secret && session_data + session_data, digest = session_data.split("--") + session_data = nil unless digest == generate_hmac(session_data) + end + + begin + session_data = session_data.unpack("m*").first + session_data = Marshal.load(session_data) + env["rack.session"] = session_data + rescue + env["rack.session"] = Hash.new + end + + env["rack.session.options"] = @default_options.dup + end + + def commit_session(env, status, headers, body) + session_data = Marshal.dump(env["rack.session"]) + session_data = [session_data].pack("m*") + + if @secret + session_data = "#{session_data}--#{generate_hmac(session_data)}" + end + + if session_data.size > (4096 - @key.size) + env["rack.errors"].puts("Warning! Rack::Session::Cookie data size exceeds 4K. Content dropped.") + [status, headers, body] + else + options = env["rack.session.options"] + cookie = Hash.new + cookie[:value] = session_data + cookie[:expires] = Time.now + options[:expire_after] unless options[:expire_after].nil? + response = Rack::Response.new(body, status, headers) + response.set_cookie(@key, cookie.merge(options)) + response.to_a + end + end + + def generate_hmac(data) + OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, @secret, data) + end + + end + end +end diff --git a/vendor/rack-0.9.1/lib/rack/session/memcache.rb b/vendor/rack-0.9.1/lib/rack/session/memcache.rb new file mode 100644 index 00000000..d19af511 --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/session/memcache.rb @@ -0,0 +1,97 @@ +# AUTHOR: blink ; blink#ruby-lang@irc.freenode.net + +require 'rack/session/abstract/id' +require 'memcache' + +module Rack + module Session + # Rack::Session::Memcache provides simple cookie based session management. + # Session data is stored in memcached. The corresponding session key is + # maintained in the cookie. + # You may treat Session::Memcache as you would Session::Pool with the + # following caveats. + # + # * Setting :expire_after to 0 would note to the Memcache server to hang + # onto the session data until it would drop it according to it's own + # specifications. However, the cookie sent to the client would expire + # immediately. + # + # Note that memcache does drop data before it may be listed to expire. For + # a full description of behaviour, please see memcache's documentation. + + class Memcache < Abstract::ID + attr_reader :mutex, :pool + DEFAULT_OPTIONS = Abstract::ID::DEFAULT_OPTIONS.merge({ + :namespace => 'rack:session', + :memcache_server => 'localhost:11211' + }) + + def initialize(app, options={}) + super + @pool = MemCache.new @default_options[:memcache_server], @default_options + unless @pool.servers.any?{|s|s.alive?} + raise "#{self} unable to find server during initialization." + end + @mutex = Mutex.new + end + + private + + def get_session(env, sid) + session = sid && @pool.get(sid) + unless session and session.is_a?(Hash) + session = {} + lc = 0 + @mutex.synchronize do + begin + raise RuntimeError, 'Unique id finding looping excessively' if (lc+=1) > 1000 + sid = generate_sid + ret = @pool.add(sid, session) + end until /^STORED/ =~ ret + end + end + class << session + @deleted = [] + def delete key + (@deleted||=[]) << key + super + end + end + [sid, session] + rescue MemCache::MemCacheError, Errno::ECONNREFUSED # MemCache server cannot be contacted + warn "#{self} is unable to find server." + warn $!.inspect + return [ nil, {} ] + end + + def set_session(env, sid) + session = env['rack.session'] + options = env['rack.session.options'] + expiry = options[:expire_after] || 0 + o, s = @mutex.synchronize do + old_session = @pool.get(sid) + unless old_session.is_a?(Hash) + warn 'Session not properly initialized.' if $DEBUG + old_session = {} + @pool.add sid, old_session, expiry + end + session.instance_eval do + @deleted.each{|k| old_session.delete(k) } if defined? @deleted + end + @pool.set sid, old_session.merge(session), expiry + [old_session, session] + end + s.each do |k,v| + next unless o.has_key?(k) and v != o[k] + warn "session value assignment collision at #{k.inspect}:"+ + "\n\t#{o[k].inspect}\n\t#{v.inspect}" + end if $DEBUG and env['rack.multithread'] + return true + rescue MemCache::MemCacheError, Errno::ECONNREFUSED # MemCache server cannot be contacted + warn "#{self} is unable to find server." + warn $!.inspect + return false + end + end + end +end diff --git a/vendor/rack-0.9.1/lib/rack/session/pool.rb b/vendor/rack-0.9.1/lib/rack/session/pool.rb new file mode 100644 index 00000000..8e192d74 --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/session/pool.rb @@ -0,0 +1,73 @@ +# AUTHOR: blink ; blink#ruby-lang@irc.freenode.net +# THANKS: +# apeiros, for session id generation, expiry setup, and threadiness +# sergio, threadiness and bugreps + +require 'rack/session/abstract/id' +require 'thread' + +module Rack + module Session + # Rack::Session::Pool provides simple cookie based session management. + # Session data is stored in a hash held by @pool. + # In the context of a multithreaded environment, sessions being + # committed to the pool is done in a merging manner. + # + # Example: + # myapp = MyRackApp.new + # sessioned = Rack::Session::Pool.new(myapp, + # :key => 'rack.session', + # :domain => 'foo.com', + # :path => '/', + # :expire_after => 2592000 + # ) + # Rack::Handler::WEBrick.run sessioned + + class Pool < Abstract::ID + attr_reader :mutex, :pool + DEFAULT_OPTIONS = Abstract::ID::DEFAULT_OPTIONS.dup + + def initialize(app, options={}) + super + @pool = Hash.new + @mutex = Mutex.new + end + + private + + def get_session(env, sid) + session = @mutex.synchronize do + unless sess = @pool[sid] and ((expires = sess[:expire_at]).nil? or expires > Time.now) + @pool.delete_if{|k,v| expiry = v[:expire_at] and expiry < Time.now } + begin + sid = generate_sid + end while @pool.has_key?(sid) + end + @pool[sid] ||= {} + end + [sid, session] + end + + def set_session(env, sid) + options = env['rack.session.options'] + expiry = options[:expire_after] && options[:at]+options[:expire_after] + @mutex.synchronize do + old_session = @pool[sid] + old_session[:expire_at] = expiry if expiry + session = old_session.merge(env['rack.session']) + @pool[sid] = session + session.each do |k,v| + next unless old_session.has_key?(k) and v != old_session[k] + warn "session value assignment collision at #{k}: #{old_session[k]} <- #{v}" + end if $DEBUG and env['rack.multithread'] + end + return true + rescue + warn "#{self} is unable to find server." + warn "#{env['rack.session'].inspect} has been lost." + warn $!.inspect + return false + end + end + end +end diff --git a/vendor/rack-0.9.1/lib/rack/showexceptions.rb b/vendor/rack-0.9.1/lib/rack/showexceptions.rb new file mode 100644 index 00000000..3fee6ae0 --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/showexceptions.rb @@ -0,0 +1,348 @@ +require 'ostruct' +require 'erb' +require 'rack/request' + +module Rack + # Rack::ShowExceptions catches all exceptions raised from the app it + # wraps. It shows a useful backtrace with the sourcefile and + # clickable context, the whole Rack environment and the request + # data. + # + # Be careful when you use this on public-facing sites as it could + # reveal information helpful to attackers. + + class ShowExceptions + CONTEXT = 7 + + def initialize(app) + @app = app + @template = ERB.new(TEMPLATE) + end + + def call(env) + @app.call(env) + rescue StandardError, LoadError, SyntaxError => e + backtrace = pretty(env, e) + [500, + {"Content-Type" => "text/html", + "Content-Length" => backtrace.join.size.to_s}, + backtrace] + end + + def pretty(env, exception) + req = Rack::Request.new(env) + path = (req.script_name + req.path_info).squeeze("/") + + frames = exception.backtrace.map { |line| + frame = OpenStruct.new + if line =~ /(.*?):(\d+)(:in `(.*)')?/ + frame.filename = $1 + frame.lineno = $2.to_i + frame.function = $4 + + begin + lineno = frame.lineno-1 + lines = ::File.readlines(frame.filename) + frame.pre_context_lineno = [lineno-CONTEXT, 0].max + frame.pre_context = lines[frame.pre_context_lineno...lineno] + frame.context_line = lines[lineno].chomp + frame.post_context_lineno = [lineno+CONTEXT, lines.size].min + frame.post_context = lines[lineno+1..frame.post_context_lineno] + rescue + end + + frame + else + nil + end + }.compact + + env["rack.errors"].puts "#{exception.class}: #{exception.message}" + env["rack.errors"].puts exception.backtrace.map { |l| "\t" + l } + env["rack.errors"].flush + + [@template.result(binding)] + end + + def h(obj) # :nodoc: + case obj + when String + Utils.escape_html(obj) + else + Utils.escape_html(obj.inspect) + end + end + + # :stopdoc: + +# adapted from Django +# Copyright (c) 2005, the Lawrence Journal-World +# Used under the modified BSD license: +# http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5 +TEMPLATE = <<'HTML' + + + + + + <%=h exception.class %> at <%=h path %> + + + + + +
+

<%=h exception.class %> at <%=h path %>

+

<%=h exception.message %>

+ + + + + + +
Ruby<%=h frames.first.filename %>: in <%=h frames.first.function %>, line <%=h frames.first.lineno %>
Web<%=h req.request_method %> <%=h(req.host + path)%>
+ +

Jump to:

+ +
+ +
+

Traceback (innermost first)

+
    +<% frames.each { |frame| %> +
  • + <%=h frame.filename %>: in <%=h frame.function %> + + <% if frame.context_line %> +
    + <% if frame.pre_context %> +
      + <% frame.pre_context.each { |line| %> +
    1. <%=h line %>
    2. + <% } %> +
    + <% end %> + +
      +
    1. <%=h frame.context_line %>...
    + + <% if frame.post_context %> +
      + <% frame.post_context.each { |line| %> +
    1. <%=h line %>
    2. + <% } %> +
    + <% end %> +
    + <% end %> +
  • +<% } %> +
+
+ +
+

Request information

+ +

GET

+ <% unless req.GET.empty? %> + + + + + + + + + <% req.GET.sort_by { |k, v| k.to_s }.each { |key, val| %> + + + + + <% } %> + +
VariableValue
<%=h key %>
<%=h val.inspect %>
+ <% else %> +

No GET data.

+ <% end %> + +

POST

+ <% unless req.POST.empty? %> + + + + + + + + + <% req.POST.sort_by { |k, v| k.to_s }.each { |key, val| %> + + + + + <% } %> + +
VariableValue
<%=h key %>
<%=h val.inspect %>
+ <% else %> +

No POST data.

+ <% end %> + + + + <% unless req.cookies.empty? %> + + + + + + + + + <% req.cookies.each { |key, val| %> + + + + + <% } %> + +
VariableValue
<%=h key %>
<%=h val.inspect %>
+ <% else %> +

No cookie data.

+ <% end %> + +

Rack ENV

+ + + + + + + + + <% env.sort_by { |k, v| k.to_s }.each { |key, val| %> + + + + + <% } %> + +
VariableValue
<%=h key %>
<%=h val %>
+ +
+ +
+

+ You're seeing this error because you use Rack::ShowException. +

+
+ + + +HTML + + # :startdoc: + end +end diff --git a/vendor/rack-0.9.1/lib/rack/showstatus.rb b/vendor/rack-0.9.1/lib/rack/showstatus.rb new file mode 100644 index 00000000..5f13404d --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/showstatus.rb @@ -0,0 +1,106 @@ +require 'erb' +require 'rack/request' +require 'rack/utils' + +module Rack + # Rack::ShowStatus catches all empty responses the app it wraps and + # replaces them with a site explaining the error. + # + # Additional details can be put into rack.showstatus.detail + # and will be shown as HTML. If such details exist, the error page + # is always rendered, even if the reply was not empty. + + class ShowStatus + def initialize(app) + @app = app + @template = ERB.new(TEMPLATE) + end + + def call(env) + status, headers, body = @app.call(env) + headers = Utils::HeaderHash.new(headers) + empty = headers['Content-Length'].to_i <= 0 + + # client or server error, or explicit message + if (status.to_i >= 400 && empty) || env["rack.showstatus.detail"] + req = Rack::Request.new(env) + message = Rack::Utils::HTTP_STATUS_CODES[status.to_i] || status.to_s + detail = env["rack.showstatus.detail"] || message + body = @template.result(binding) + size = body.respond_to?(:bytesize) ? body.bytesize : body.size + [status, headers.merge("Content-Type" => "text/html", "Content-Length" => size.to_s), [body]] + else + [status, headers, body] + end + end + + def h(obj) # :nodoc: + case obj + when String + Utils.escape_html(obj) + else + Utils.escape_html(obj.inspect) + end + end + + # :stopdoc: + +# adapted from Django +# Copyright (c) 2005, the Lawrence Journal-World +# Used under the modified BSD license: +# http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5 +TEMPLATE = <<'HTML' + + + + + <%=h message %> at <%=h req.script_name + req.path_info %> + + + + +
+

<%=h message %> (<%= status.to_i %>)

+ + + + + + + + + +
Request Method:<%=h req.request_method %>
Request URL:<%=h req.url %>
+
+
+

<%= detail %>

+
+ +
+

+ You're seeing this error because you use Rack::ShowStatus. +

+
+ + +HTML + + # :startdoc: + end +end diff --git a/vendor/rack-0.9.1/lib/rack/static.rb b/vendor/rack-0.9.1/lib/rack/static.rb new file mode 100644 index 00000000..168e8f83 --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/static.rb @@ -0,0 +1,38 @@ +module Rack + + # The Rack::Static middleware intercepts requests for static files + # (javascript files, images, stylesheets, etc) based on the url prefixes + # passed in the options, and serves them using a Rack::File object. This + # allows a Rack stack to serve both static and dynamic content. + # + # Examples: + # use Rack::Static, :urls => ["/media"] + # will serve all requests beginning with /media from the "media" folder + # located in the current directory (ie media/*). + # + # use Rack::Static, :urls => ["/css", "/images"], :root => "public" + # will serve all requests beginning with /css or /images from the folder + # "public" in the current directory (ie public/css/* and public/images/*) + + class Static + + def initialize(app, options={}) + @app = app + @urls = options[:urls] || ["/favicon.ico"] + root = options[:root] || Dir.pwd + @file_server = Rack::File.new(root) + end + + def call(env) + path = env["PATH_INFO"] + can_serve = @urls.any? { |url| path.index(url) == 0 } + + if can_serve + @file_server.call(env) + else + @app.call(env) + end + end + + end +end diff --git a/vendor/rack-0.9.1/lib/rack/urlmap.rb b/vendor/rack-0.9.1/lib/rack/urlmap.rb new file mode 100644 index 00000000..01c9603e --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/urlmap.rb @@ -0,0 +1,48 @@ +module Rack + # Rack::URLMap takes a hash mapping urls or paths to apps, and + # dispatches accordingly. Support for HTTP/1.1 host names exists if + # the URLs start with http:// or https://. + # + # URLMap modifies the SCRIPT_NAME and PATH_INFO such that the part + # relevant for dispatch is in the SCRIPT_NAME, and the rest in the + # PATH_INFO. This should be taken care of when you need to + # reconstruct the URL in order to create links. + # + # URLMap dispatches in such a way that the longest paths are tried + # first, since they are most specific. + + class URLMap + def initialize(map) + @mapping = map.map { |location, app| + if location =~ %r{\Ahttps?://(.*?)(/.*)} + host, location = $1, $2 + else + host = nil + end + + unless location[0] == ?/ + raise ArgumentError, "paths need to start with /" + end + location = location.chomp('/') + + [host, location, app] + }.sort_by { |(h, l, a)| [-l.size, h.to_s.size] } # Longest path first + end + + def call(env) + path = env["PATH_INFO"].to_s.squeeze("/") + hHost, sName, sPort = env.values_at('HTTP_HOST','SERVER_NAME','SERVER_PORT') + @mapping.each { |host, location, app| + next unless (hHost == host || sName == host \ + || (host.nil? && (hHost == sName || hHost == sName+':'+sPort))) + next unless location == path[0, location.size] + next unless path[location.size] == nil || path[location.size] == ?/ + env["SCRIPT_NAME"] += location + env["PATH_INFO"] = path[location.size..-1] + return app.call(env) + } + [404, {"Content-Type" => "text/plain"}, ["Not Found: #{path}"]] + end + end +end + diff --git a/vendor/rack-0.9.1/lib/rack/utils.rb b/vendor/rack-0.9.1/lib/rack/utils.rb new file mode 100644 index 00000000..3fb7a703 --- /dev/null +++ b/vendor/rack-0.9.1/lib/rack/utils.rb @@ -0,0 +1,347 @@ +require 'set' +require 'tempfile' + +module Rack + # Rack::Utils contains a grab-bag of useful methods for writing web + # applications adopted from all kinds of Ruby libraries. + + module Utils + # Performs URI escaping so that you can construct proper + # query strings faster. Use this rather than the cgi.rb + # version since it's faster. (Stolen from Camping). + def escape(s) + s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) { + '%'+$1.unpack('H2'*$1.size).join('%').upcase + }.tr(' ', '+') + end + module_function :escape + + # Unescapes a URI escaped string. (Stolen from Camping). + def unescape(s) + s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){ + [$1.delete('%')].pack('H*') + } + end + module_function :unescape + + # Stolen from Mongrel, with some small modifications: + # Parses a query string by breaking it up at the '&' + # and ';' characters. You can also use this to parse + # cookies by changing the characters used in the second + # parameter (which defaults to '&;'). + + def parse_query(qs, d = '&;') + params = {} + + (qs || '').split(/[#{d}] */n).each do |p| + k, v = unescape(p).split('=', 2) + + if cur = params[k] + if cur.class == Array + params[k] << v + else + params[k] = [cur, v] + end + else + params[k] = v + end + end + + return params + end + module_function :parse_query + + def build_query(params) + params.map { |k, v| + if v.class == Array + build_query(v.map { |x| [k, x] }) + else + escape(k) + "=" + escape(v) + end + }.join("&") + end + module_function :build_query + + # Escape ampersands, brackets and quotes to their HTML/XML entities. + def escape_html(string) + string.to_s.gsub("&", "&"). + gsub("<", "<"). + gsub(">", ">"). + gsub("'", "'"). + gsub('"', """) + end + module_function :escape_html + + def select_best_encoding(available_encodings, accept_encoding) + # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html + + expanded_accept_encoding = + accept_encoding.map { |m, q| + if m == "*" + (available_encodings - accept_encoding.map { |m2, _| m2 }).map { |m2| [m2, q] } + else + [[m, q]] + end + }.inject([]) { |mem, list| + mem + list + } + + encoding_candidates = expanded_accept_encoding.sort_by { |_, q| -q }.map { |m, _| m } + + unless encoding_candidates.include?("identity") + encoding_candidates.push("identity") + end + + expanded_accept_encoding.find_all { |m, q| + q == 0.0 + }.each { |m, _| + encoding_candidates.delete(m) + } + + return (encoding_candidates & available_encodings)[0] + end + module_function :select_best_encoding + + # The recommended manner in which to implement a contexting application + # is to define a method #context in which a new Context is instantiated. + # + # As a Context is a glorified block, it is highly recommended that you + # define the contextual block within the application's operational scope. + # This would typically the application as you're place into Rack's stack. + # + # class MyObject + # ... + # def context app + # Rack::Utils::Context.new app do |env| + # do_stuff + # response = app.call(env) + # do_more_stuff + # end + # end + # ... + # end + # + # mobj = MyObject.new + # app = mobj.context other_app + # Rack::Handler::Mongrel.new app + class Context < Proc + alias_method :old_inspect, :inspect + attr_reader :for, :app + def initialize app_f, app_r + raise 'running context not provided' unless app_f + raise 'running context does not respond to #context' unless app_f.respond_to? :context + raise 'application context not provided' unless app_r + raise 'application context does not respond to #call' unless app_r.respond_to? :call + @for = app_f + @app = app_r + end + def inspect + "#{old_inspect} ==> #{@for.inspect} ==> #{@app.inspect}" + end + def context app_r + raise 'new application context not provided' unless app_r + raise 'new application context does not respond to #call' unless app_r.respond_to? :call + @for.context app_r + end + def pretty_print pp + pp.text old_inspect + pp.nest 1 do + pp.breakable + pp.text '=for> ' + pp.pp @for + pp.breakable + pp.text '=app> ' + pp.pp @app + end + end + end + + # A case-insensitive Hash that preserves the original case of a + # header when set. + class HeaderHash < Hash + def initialize(hash={}) + @names = {} + hash.each { |k, v| self[k] = v } + end + + def to_hash + {}.replace(self) + end + + def [](k) + super @names[k.downcase] + end + + def []=(k, v) + delete k + @names[k.downcase] = k + super k, v + end + + def delete(k) + super @names.delete(k.downcase) + end + + def include?(k) + @names.has_key? k.downcase + end + + alias_method :has_key?, :include? + alias_method :member?, :include? + alias_method :key?, :include? + + def merge!(other) + other.each { |k, v| self[k] = v } + self + end + + def merge(other) + hash = dup + hash.merge! other + end + end + + # Every standard HTTP code mapped to the appropriate message. + # Stolen from Mongrel. + HTTP_STATUS_CODES = { + 100 => 'Continue', + 101 => 'Switching Protocols', + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 307 => 'Temporary Redirect', + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Large', + 415 => 'Unsupported Media Type', + 416 => 'Requested Range Not Satisfiable', + 417 => 'Expectation Failed', + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version Not Supported' + } + + # Responses with HTTP status codes that should not have an entity body + STATUS_WITH_NO_ENTITY_BODY = Set.new((100..199).to_a << 204 << 304) + + # A multipart form data parser, adapted from IOWA. + # + # Usually, Rack::Request#POST takes care of calling this. + + module Multipart + EOL = "\r\n" + + def self.parse_multipart(env) + unless env['CONTENT_TYPE'] =~ + %r|\Amultipart/form-data.*boundary=\"?([^\";,]+)\"?|n + nil + else + boundary = "--#{$1}" + + params = {} + buf = "" + content_length = env['CONTENT_LENGTH'].to_i + input = env['rack.input'] + + boundary_size = boundary.size + EOL.size + bufsize = 16384 + + content_length -= boundary_size + + status = input.read(boundary_size) + raise EOFError, "bad content body" unless status == boundary + EOL + + rx = /(?:#{EOL})?#{Regexp.quote boundary}(#{EOL}|--)/ + + loop { + head = nil + body = '' + filename = content_type = name = nil + + until head && buf =~ rx + if !head && i = buf.index("\r\n\r\n") + head = buf.slice!(0, i+2) # First \r\n + buf.slice!(0, 2) # Second \r\n + + filename = head[/Content-Disposition:.* filename="?([^\";]*)"?/ni, 1] + content_type = head[/Content-Type: (.*)\r\n/ni, 1] + name = head[/Content-Disposition:.* name="?([^\";]*)"?/ni, 1] + + if filename + body = Tempfile.new("RackMultipart") + body.binmode if body.respond_to?(:binmode) + end + + next + end + + # Save the read body part. + if head && (boundary_size+4 < buf.size) + body << buf.slice!(0, buf.size - (boundary_size+4)) + end + + c = input.read(bufsize < content_length ? bufsize : content_length) + raise EOFError, "bad content body" if c.nil? || c.empty? + buf << c + content_length -= c.size + end + + # Save the rest. + if i = buf.index(rx) + body << buf.slice!(0, i) + buf.slice!(0, boundary_size+2) + + content_length = -1 if $1 == "--" + end + + if filename + body.rewind + data = {:filename => filename, :type => content_type, + :name => name, :tempfile => body, :head => head} + else + data = body + end + + if name + if name =~ /\[\]\z/ + params[name] ||= [] + params[name] << data + else + params[name] = data + end + end + + break if buf.empty? || content_length == -1 + } + + params + end + end + end + end +end