Ensure request is sent even if ebb_client_body_write isn't called
[ebb.git] / ruby_lib / ebb.rb
blob13341a41ce94aed7e17fb8dbdcd31e64fed1d3bc
1 # Ruby Binding to the Ebb Web Server
2 # Copyright (c) 2008 Ry Dahl. This software is released under the MIT License.
3 # See README file for details.
4 require 'stringio'
5 module Ebb
6   VERSION = "0.2.0"
7   LIBDIR = File.dirname(__FILE__)
8   autoload :Runner, LIBDIR + '/ebb/runner'
9   autoload :FFI, LIBDIR + '/../src/ebb_ext'
10   
11   def self.start_server(app, options={})
12     if options.has_key?(:fileno)
13       fd = options[:fileno].to_i
14       FFI::server_listen_on_fd(fd)
15       log.puts "Ebb is listening on file descriptor #{fd}"
16     elsif options.has_key?(:unix_socket)
17       socketfile = options[:unix_socket]
18       FFI::server_listen_on_unix_socket(socketfile)
19       log.puts "Ebb is listening on unix socket #{socketfile}"
20     else
21       port = (options[:port] || 4001).to_i
22       FFI::server_listen_on_port(port)
23       log.puts "Ebb is listening at http://0.0.0.0:#{port}/"
24     end
25     log.puts "Ebb PID #{Process.pid}"
26     
27     @running = true
28     trap('INT')  { stop_server }
29     
30     while @running
31       FFI::server_process_connections()
32       while client = FFI::server_waiting_clients.shift
33         if app.respond_to?(:deferred?) and !app.deferred?(client.env)
34           process(app, client)
35         else
36           Thread.new(client) { |c| process(app, c) }
37         end
38       end
39     end
40     FFI::server_unlisten()
41   end
42   
43   def self.running?
44     FFI::server_open?
45   end
46   
47   def self.stop_server()
48     @running = false
49   end
50   
51   def self.process(app, client)
52     #p client.env
53     status, headers, body = app.call(client.env)
54     
55     # Write the status
56     client.write_status(status)
57     
58     # Add Content-Length to the headers.
59     if headers['Content-Length'].nil? and
60        headers.respond_to?(:[]=) and 
61        body.respond_to?(:length) and 
62        status != 304
63     then
64       headers['Content-Length'] = body.length.to_s
65     end
66     
67     # Decide if we should keep the connection alive or not
68     if headers['Connection'].nil?
69       if headers['Content-Length'] and client.should_keep_alive?
70         headers['Connection'] = 'Keep-Alive'
71       else
72         headers['Connection'] = 'close'
73       end
74     end
75     
76     # Write the headers
77     headers.each { |field, value| client.write_header(field, value) }
78     
79     # Write the body
80     if body.kind_of?(String)
81       client.write_body(body)
82     else
83       body.each { |p| client.write_body(p) }
84     end
85     
86   rescue => e
87     log.puts "Ebb Error! #{e.class}  #{e.message}"
88     log.puts e.backtrace.join("\n")
89   ensure
90     client.release
91   end
92   
93   @@log = STDOUT
94   def self.log=(output)
95     @@log = output
96   end
97   def self.log
98     @@log
99   end
100   
101   class Client
102     attr_reader :fd, :body_head, :content_length
103     BASE_ENV = {
104       'SERVER_NAME' => '0.0.0.0',
105       'SCRIPT_NAME' => '',
106       'SERVER_SOFTWARE' => "Ebb-Ruby #{Ebb::VERSION}",
107       'SERVER_PROTOCOL' => 'HTTP/1.1',
108       'rack.version' => [0, 1],
109       'rack.errors' => STDERR,
110       'rack.url_scheme' => 'http',
111       'rack.multiprocess' => false,
112       'rack.run_once' => false
113     }
114     
115     def env
116       @env ||= begin
117         env = FFI::client_env(self).update(BASE_ENV)
118         env['rack.input'] = RequestBody.new(self)
119         env
120       end
121     end
122     
123     def write_status(status)
124       s = status.to_i
125       FFI::client_write_status(self, s, HTTP_STATUS_CODES[s])
126     end
127     
128     def write_body(data)
129       FFI::client_write_body(self, data)
130     end
131     
132     def write_header(field, value)
133       value.send(value.is_a?(String) ? :each_line : :each) do |v| 
134         FFI::client_write_header(self, field, v.chomp)
135       end
136     end
137     
138     def release
139       FFI::client_release(self)
140     end
141     
142     def set_keep_alive
143       FFI::client_set_keep_alive(self)
144     end
145     
146     def should_keep_alive?
147       if env['HTTP_VERSION'] == 'HTTP/1.0' 
148         return true if env['HTTP_CONNECTION'] =~ /Keep-Alive/i
149       else
150         return true unless env['HTTP_CONNECTION'] =~ /close/i
151       end
152       false
153     end
154   end
155   
156   class RequestBody
157     def initialize(client)
158       @content_length = client.content_length
159       if client.body_head
160         @body_head = StringIO.new(client.body_head)
161         if @body_head.length < @content_length
162           @socket = IO.new(client.fd)
163         end
164       end
165       @total_read = 0
166     end
167     
168     def read(len = nil)
169       to_read =  len.nil? ? @content_length - @total_read : min(len, @content_length - @total_read)
170       return nil if to_read == 0 or @body_head.nil?
171       unless out = @body_head.read(to_read)
172         return nil if @socket.nil?
173         out = @socket.read(to_read)
174       end
175       @total_read += out.length
176       out
177     end
178     
179     def gets
180       raise NotImplemented
181     end
182     
183     def each(&block)
184       raise NotImplemented
185     end
186   end
187   
188   
189   HTTP_STATUS_CODES = {  
190     100  => 'Continue', 
191     101  => 'Switching Protocols', 
192     200  => 'OK', 
193     201  => 'Created', 
194     202  => 'Accepted', 
195     203  => 'Non-Authoritative Information', 
196     204  => 'No Content', 
197     205  => 'Reset Content', 
198     206  => 'Partial Content', 
199     300  => 'Multiple Choices', 
200     301  => 'Moved Permanently', 
201     302  => 'Moved Temporarily', 
202     303  => 'See Other', 
203     304  => 'Not Modified', 
204     305  => 'Use Proxy', 
205     400  => 'Bad Request', 
206     401  => 'Unauthorized', 
207     402  => 'Payment Required', 
208     403  => 'Forbidden', 
209     404  => 'Not Found', 
210     405  => 'Method Not Allowed', 
211     406  => 'Not Acceptable', 
212     407  => 'Proxy Authentication Required', 
213     408  => 'Request Time-out', 
214     409  => 'Conflict', 
215     410  => 'Gone', 
216     411  => 'Length Required', 
217     412  => 'Precondition Failed', 
218     413  => 'Request Entity Too Large', 
219     414  => 'Request-URI Too Large', 
220     415  => 'Unsupported Media Type', 
221     500  => 'Internal Server Error', 
222     501  => 'Not Implemented', 
223     502  => 'Bad Gateway', 
224     503  => 'Service Unavailable', 
225     504  => 'Gateway Time-out', 
226     505  => 'HTTP Version not supported'
227   }.freeze
231 module Rack
232   module Handler
233     module Ebb
234       def self.run(app, options={})
235         ::Ebb.start_server(app, options)
236       end
237     end
238   end
241 # cause i don't want to create an array
242 def min(a,b)
243   a > b ? b : a