| 1 | # == About camping/fastcgi.rb |
|---|
| 2 | # |
|---|
| 3 | # Camping works very well with FastCGI, since your application is only loaded |
|---|
| 4 | # once -- when FastCGI starts. In addition, this class lets you mount several |
|---|
| 5 | # Camping apps under a single FastCGI process, to help save memory costs. |
|---|
| 6 | # |
|---|
| 7 | # So where do you use the Camping::FastCGI class? Use it in your application's |
|---|
| 8 | # postamble and then you can point your web server directly at your application. |
|---|
| 9 | # See Camping::FastCGI docs for more. |
|---|
| 10 | require 'camping' |
|---|
| 11 | require 'fcgi' |
|---|
| 12 | |
|---|
| 13 | module Camping |
|---|
| 14 | # Camping::FastCGI is a small class for hooking one or more Camping apps up to |
|---|
| 15 | # FastCGI. Generally, you'll use this class in your application's postamble. |
|---|
| 16 | # |
|---|
| 17 | # == The Smallest Example |
|---|
| 18 | # |
|---|
| 19 | # if __FILE__ == $0 |
|---|
| 20 | # require 'camping/fastcgi' |
|---|
| 21 | # Camping::FastCGI.start(YourApp) |
|---|
| 22 | # end |
|---|
| 23 | # |
|---|
| 24 | # This example is stripped down to the basics. The postamble has no database |
|---|
| 25 | # connection. It just loads this class and calls Camping::FastCGI.start. |
|---|
| 26 | # |
|---|
| 27 | # Now, in Lighttpd or Apache, you can point to your app's file, which will |
|---|
| 28 | # be executed, only to discover that your app now speaks the FastCGI protocol. |
|---|
| 29 | # |
|---|
| 30 | # Here's a sample lighttpd.conf (tested with Lighttpd 1.4.11) to serve as example: |
|---|
| 31 | # |
|---|
| 32 | # server.port = 3044 |
|---|
| 33 | # server.bind = "127.0.0.1" |
|---|
| 34 | # server.modules = ( "mod_fastcgi" ) |
|---|
| 35 | # server.document-root = "/var/www/camping/blog/" |
|---|
| 36 | # server.errorlog = "/var/www/camping/blog/error.log" |
|---|
| 37 | # |
|---|
| 38 | # #### fastcgi module |
|---|
| 39 | # fastcgi.server = ( "/" => ( |
|---|
| 40 | # "localhost" => ( |
|---|
| 41 | # "socket" => "/tmp/camping-blog.socket", |
|---|
| 42 | # "bin-path" => "/var/www/camping/blog/blog.rb", |
|---|
| 43 | # "check-local" => "disable", |
|---|
| 44 | # "max-procs" => 1 ) ) ) |
|---|
| 45 | # |
|---|
| 46 | # The file <tt>/var/www/camping/blog/blog.rb</tt> is the Camping app with |
|---|
| 47 | # the postamble. |
|---|
| 48 | # |
|---|
| 49 | # == Mounting Many Apps |
|---|
| 50 | # |
|---|
| 51 | # require 'camping/fastcgi' |
|---|
| 52 | # fast = Camping::FastCGI.new |
|---|
| 53 | # fast.mount("/blog", Blog) |
|---|
| 54 | # fast.mount("/tepee", Tepee) |
|---|
| 55 | # fast.mount("/", Index) |
|---|
| 56 | # fast.start |
|---|
| 57 | # |
|---|
| 58 | class FastCGI |
|---|
| 59 | CHUNK_SIZE=(4 * 1024) |
|---|
| 60 | |
|---|
| 61 | attr_reader :mounts |
|---|
| 62 | |
|---|
| 63 | # Creates a Camping::FastCGI class with empty mounts. |
|---|
| 64 | def initialize |
|---|
| 65 | @mounts = {} |
|---|
| 66 | end |
|---|
| 67 | # Mounts a Camping application. The +dir+ being the name of the directory |
|---|
| 68 | # to serve as the application's root. The +app+ is a Camping class. |
|---|
| 69 | def mount(dir, app) |
|---|
| 70 | dir.gsub!(/\/{2,}/, '/') |
|---|
| 71 | dir.gsub!(/\/+$/, '') |
|---|
| 72 | @mounts[dir] = app |
|---|
| 73 | end |
|---|
| 74 | |
|---|
| 75 | # |
|---|
| 76 | # Starts the FastCGI main loop. |
|---|
| 77 | def start(&blk) |
|---|
| 78 | FCGI.each do |req| |
|---|
| 79 | camp_do(req, &blk) |
|---|
| 80 | end |
|---|
| 81 | end |
|---|
| 82 | |
|---|
| 83 | # A simple single-app starter mechanism |
|---|
| 84 | # |
|---|
| 85 | # Camping::FastCGI.start(Blog) |
|---|
| 86 | # |
|---|
| 87 | def self.start(app) |
|---|
| 88 | cf = Camping::FastCGI.new |
|---|
| 89 | cf.mount("/", app) |
|---|
| 90 | cf.start |
|---|
| 91 | end |
|---|
| 92 | |
|---|
| 93 | # Serve an entire directory of Camping apps. (See |
|---|
| 94 | # http://code.whytheluckystiff.net/camping/wiki/TheCampingServer.) |
|---|
| 95 | # |
|---|
| 96 | # Use this method inside your FastCGI dispatcher: |
|---|
| 97 | # |
|---|
| 98 | # #!/usr/local/bin/ruby |
|---|
| 99 | # require 'rubygems' |
|---|
| 100 | # require 'camping/fastcgi' |
|---|
| 101 | # Camping::Models::Base.establish_connection :adapter => 'sqlite3', :database => "/path/to/db" |
|---|
| 102 | # Camping::FastCGI.serve("/home/why/cvs/camping/examples") |
|---|
| 103 | # |
|---|
| 104 | def self.serve(path, index=nil) |
|---|
| 105 | require 'camping/reloader' |
|---|
| 106 | if File.directory? path |
|---|
| 107 | fast = Camping::FastCGI.new |
|---|
| 108 | script_load = proc do |script| |
|---|
| 109 | app = Camping::Reloader.new(script) |
|---|
| 110 | fast.mount("/#{app.mount}", app) |
|---|
| 111 | app |
|---|
| 112 | end |
|---|
| 113 | Dir[File.join(path, '*.rb')].each &script_load |
|---|
| 114 | fast.mount("/", index) if index |
|---|
| 115 | |
|---|
| 116 | fast.start do |dir, app| |
|---|
| 117 | Dir[File.join(path, dir, '*.rb')].each do |script| |
|---|
| 118 | smount = "/" + File.basename(script, '.rb') |
|---|
| 119 | script_load[script] unless fast.mounts.has_key? smount |
|---|
| 120 | end |
|---|
| 121 | end |
|---|
| 122 | else |
|---|
| 123 | start(Camping::Reloader.new(path)) |
|---|
| 124 | end |
|---|
| 125 | end |
|---|
| 126 | |
|---|
| 127 | private |
|---|
| 128 | |
|---|
| 129 | def camp_do(req) |
|---|
| 130 | root, path, dir, app = "/" |
|---|
| 131 | if ENV['FORCE_ROOT'] and ENV['FORCE_ROOT'].to_i == 1 |
|---|
| 132 | path = req.env['SCRIPT_NAME'] |
|---|
| 133 | else |
|---|
| 134 | root = req.env['SCRIPT_NAME'] |
|---|
| 135 | path = req.env['PATH_INFO'] |
|---|
| 136 | end |
|---|
| 137 | |
|---|
| 138 | dir, app = @mounts.max { |a,b| match(path, a[0]) <=> match(path, b[0]) } |
|---|
| 139 | unless dir and app |
|---|
| 140 | dir, app = '/', Camping |
|---|
| 141 | end |
|---|
| 142 | yield dir, app if block_given? |
|---|
| 143 | |
|---|
| 144 | req.env['SERVER_SCRIPT_NAME'] = req.env['SCRIPT_NAME'] |
|---|
| 145 | req.env['SERVER_PATH_INFO'] = req.env['PATH_INFO'] |
|---|
| 146 | req.env['SCRIPT_NAME'] = File.join(root, dir) |
|---|
| 147 | req.env['PATH_INFO'] = path.gsub(/^#{dir}/, '') |
|---|
| 148 | |
|---|
| 149 | controller = app.run(SeekStream.new(req.in), req.env) |
|---|
| 150 | sendfile = nil |
|---|
| 151 | headers = {} |
|---|
| 152 | controller.headers.each do |k, v| |
|---|
| 153 | if k =~ /^X-SENDFILE$/i and !ENV['SERVER_X_SENDFILE'] |
|---|
| 154 | sendfile = v |
|---|
| 155 | else |
|---|
| 156 | headers[k] = v |
|---|
| 157 | end |
|---|
| 158 | end |
|---|
| 159 | |
|---|
| 160 | body = controller.body |
|---|
| 161 | controller.body = "" |
|---|
| 162 | controller.headers = headers |
|---|
| 163 | |
|---|
| 164 | req.out << controller.to_s |
|---|
| 165 | if sendfile |
|---|
| 166 | File.open(sendfile, "rb") do |f| |
|---|
| 167 | while chunk = f.read(CHUNK_SIZE) and chunk.length > 0 |
|---|
| 168 | req.out << chunk |
|---|
| 169 | end |
|---|
| 170 | end |
|---|
| 171 | elsif body.respond_to? :read |
|---|
| 172 | while chunk = body.read(CHUNK_SIZE) and chunk.length > 0 |
|---|
| 173 | req.out << chunk |
|---|
| 174 | end |
|---|
| 175 | body.close if body.respond_to? :close |
|---|
| 176 | else |
|---|
| 177 | req.out << body.to_s |
|---|
| 178 | end |
|---|
| 179 | rescue Errno::EPIPE, EOFError |
|---|
| 180 | rescue SystemExit |
|---|
| 181 | raise |
|---|
| 182 | rescue Exception => exc |
|---|
| 183 | req.out << server_error(root, path, exc, req) |
|---|
| 184 | ensure |
|---|
| 185 | req.finish |
|---|
| 186 | end |
|---|
| 187 | |
|---|
| 188 | def server_error(root, path, exc, req) |
|---|
| 189 | "Content-Type: text/html\r\n\r\n" + |
|---|
| 190 | "<h1>Camping Problem!</h1>" + |
|---|
| 191 | "<h2><strong>#{root}</strong>#{path}</h2>" + |
|---|
| 192 | "<h3>#{exc.class} #{esc exc.message}</h3>" + |
|---|
| 193 | "<ul>" + exc.backtrace.map { |bt| "<li>#{esc bt}</li>" }.join + "</ul>" + |
|---|
| 194 | "<hr /><p>#{req.env.inspect}</p>" |
|---|
| 195 | end |
|---|
| 196 | |
|---|
| 197 | def match(path, mount) |
|---|
| 198 | m = path.match(/^#{Regexp::quote mount}(\/|$)/) |
|---|
| 199 | if m; m.end(0) |
|---|
| 200 | else -1 |
|---|
| 201 | end |
|---|
| 202 | end |
|---|
| 203 | |
|---|
| 204 | def esc(str) |
|---|
| 205 | str.gsub(/&/n, '&').gsub(/\"/n, '"').gsub(/>/n, '>').gsub(/</n, '<') |
|---|
| 206 | end |
|---|
| 207 | |
|---|
| 208 | class SeekStream |
|---|
| 209 | def initialize(stream) |
|---|
| 210 | @last_read = "" |
|---|
| 211 | @stream = stream |
|---|
| 212 | @buffer = "" |
|---|
| 213 | end |
|---|
| 214 | def eof? |
|---|
| 215 | @buffer.empty? && @stream.eof? |
|---|
| 216 | end |
|---|
| 217 | def each |
|---|
| 218 | while true |
|---|
| 219 | pull(1024) until eof? or @buffer.index("\n") |
|---|
| 220 | return nil if eof? |
|---|
| 221 | yield @buffer.slice!(0..(@buffer.index("\n") || -1)) |
|---|
| 222 | end |
|---|
| 223 | end |
|---|
| 224 | def pull(len) |
|---|
| 225 | @buffer += @stream.read(len).to_s |
|---|
| 226 | end |
|---|
| 227 | def read(len = 16384) |
|---|
| 228 | pull(len) |
|---|
| 229 | @last_read = |
|---|
| 230 | if eof? |
|---|
| 231 | nil |
|---|
| 232 | else |
|---|
| 233 | @buffer.slice!(0...len) |
|---|
| 234 | end |
|---|
| 235 | end |
|---|
| 236 | def seek(len, typ) |
|---|
| 237 | raise NotImplementedError, "only IO::SEEK_CUR is supported with SeekStream" if typ != IO::SEEK_CUR |
|---|
| 238 | raise NotImplementedError, "only rewinding is supported with SeekStream" if len > 0 |
|---|
| 239 | raise NotImplementedError, "rewinding #{-len} past the buffer #{@last_read.size} start not supported with SeekStream" if -len > @last_read.size |
|---|
| 240 | @buffer = @last_read[len..-1] + @buffer |
|---|
| 241 | @last_read = "" |
|---|
| 242 | self |
|---|
| 243 | end |
|---|
| 244 | end |
|---|
| 245 | |
|---|
| 246 | end |
|---|
| 247 | end |
|---|