root / trunk / lib / camping / fastcgi.rb

Revision 190, 7.6 kB (checked in by why, 17 months ago)
Line 
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.
10require 'camping'
11require 'fcgi'
12
13module 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#
58class 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, '&amp;').gsub(/\"/n, '&quot;').gsub(/>/n, '&gt;').gsub(/</n, '&lt;')
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
246end
247end
Note: See TracBrowser for help on using the browser.