Fix up tests and runner
[ebb.git] / ruby_lib / ebb.rb
blob4378dbb4420c9dc3a115d730ac0ee7b3518c9964
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   LIBDIR = File.dirname(__FILE__)
7   require Ebb::LIBDIR + '/../src/ebb_ext'
8   autoload :Runner, LIBDIR + '/ebb/runner'
9   
10   def self.start_server(app, options={})
11     port = (options[:port] || 4001).to_i
12     if options.has_key?(:threaded_processing)
13       threaded_processing = options[:threaded_processing] ? true : false
14     else
15       threaded_processing = true
16     end
17     
18     Client::BASE_ENV['rack.multithread'] = threaded_processing
19     
20     FFI::server_listen_on_port(port)
21     @running = true
22     trap('INT')  { stop_server }
23     
24     log.puts "Ebb listening at http://0.0.0.0:#{port}/ (#{threaded_processing ? 'threaded' : 'sequential'} processing, PID #{Process.pid})"
25     
26     while @running
27       FFI::server_process_connections()
28       while client = FFI::waiting_clients.shift
29         if threaded_processing
30           Thread.new(client) { |c| process(app, c) }
31         else
32           process(app, client)
33         end
34       end
35     end
36     FFI::server_unlisten()
37   end
38   
39   def self.running?
40     FFI::server_open?
41   end
42   
43   def self.stop_server()
44     @running = false
45   end
46   
47   def self.process(app, client)
48     begin
49       status, headers, body = app.call(client.env)
50     rescue
51       raise if $DEBUG
52       status = 500
53       headers = {'Content-Type' => 'text/plain'}
54       body = "Internal Server Error\n"
55     end
56     
57     client.write_status(status)
58     
59     if headers.respond_to?(:[]=) and body.respond_to?(:length) and status != 304
60       headers['Connection'] = 'close'
61       headers['Content-Length'] = body.length.to_s
62     end
63     
64     headers.each { |field, value| client.write_header(field, value) }
65     client.write("\r\n")
66     
67     if body.kind_of?(String)
68       client.write(body)
69       client.body_written()
70       client.begin_transmission()
71     else
72       client.begin_transmission()
73       body.each { |p| client.write(p) }
74       client.body_written()
75     end
76   rescue => e
77     log.puts "Ebb Error! #{e.class}  #{e.message}"
78     log.puts e.backtrace.join("\n")
79   ensure
80     client.release
81   end
82   
83   @@log = STDOUT
84   def self.log=(output)
85     @@log = output
86   end
87   def self.log
88     @@log
89   end
90   
91   # This array is created and manipulated in the C extension.
92   def FFI.waiting_clients
93     @waiting_clients
94   end
95   
96   class Client
97     BASE_ENV = {
98       'SERVER_NAME' => '0.0.0.0',
99       'SCRIPT_NAME' => '',
100       'SERVER_SOFTWARE' => "Ebb #{Ebb::VERSION}",
101       'SERVER_PROTOCOL' => 'HTTP/1.1',
102       'rack.version' => [0, 1],
103       'rack.errors' => STDERR,
104       'rack.url_scheme' => 'http',
105       'rack.multiprocess' => false,
106       'rack.run_once' => false
107     }
108     
109     def env
110       env = FFI::client_env(self).update(BASE_ENV)
111       env['rack.input'] = RequestBody.new(self)
112       env
113     end
114     
115     def write_status(status)
116       s = status.to_i
117       FFI::client_write_status(self, s, HTTP_STATUS_CODES[s])
118     end
119     
120     def write(data)
121       FFI::client_write(self, data)
122     end
123     
124     def write_header(field, value)
125       value.send(value.is_a?(String) ? :each_line : :each) do |v| 
126         FFI::client_write_header(self, field, v.chomp)
127       end
128     end
129     
130     def body_written
131       FFI::client_set_body_written(self, true)
132     end
133     
134     def begin_transmission
135       FFI::client_begin_transmission(self)
136     end
137     
138     def release
139       FFI::client_release(self)
140     end
141   end
142   
143   class RequestBody
144     def initialize(client)
145       @client = client
146     end
147     
148     def read(len = nil)
149       if @io
150         @io.read(len)
151       else
152         if len.nil?
153           s = ''
154           while(chunk = read(10*1024)) do
155             s << chunk
156           end
157           s
158         else
159           FFI::client_read_input(@client, len)
160         end
161       end
162     end
163     
164     def gets
165       io.gets
166     end
167     
168     def each(&block)
169       io.each(&block)
170     end
171     
172     def io
173       @io ||= StringIO.new(read)
174     end
175   end
176   
177   
178   HTTP_STATUS_CODES = {  
179     100  => 'Continue', 
180     101  => 'Switching Protocols', 
181     200  => 'OK', 
182     201  => 'Created', 
183     202  => 'Accepted', 
184     203  => 'Non-Authoritative Information', 
185     204  => 'No Content', 
186     205  => 'Reset Content', 
187     206  => 'Partial Content', 
188     300  => 'Multiple Choices', 
189     301  => 'Moved Permanently', 
190     302  => 'Moved Temporarily', 
191     303  => 'See Other', 
192     304  => 'Not Modified', 
193     305  => 'Use Proxy', 
194     400  => 'Bad Request', 
195     401  => 'Unauthorized', 
196     402  => 'Payment Required', 
197     403  => 'Forbidden', 
198     404  => 'Not Found', 
199     405  => 'Method Not Allowed', 
200     406  => 'Not Acceptable', 
201     407  => 'Proxy Authentication Required', 
202     408  => 'Request Time-out', 
203     409  => 'Conflict', 
204     410  => 'Gone', 
205     411  => 'Length Required', 
206     412  => 'Precondition Failed', 
207     413  => 'Request Entity Too Large', 
208     414  => 'Request-URI Too Large', 
209     415  => 'Unsupported Media Type', 
210     500  => 'Internal Server Error', 
211     501  => 'Not Implemented', 
212     502  => 'Bad Gateway', 
213     503  => 'Service Unavailable', 
214     504  => 'Gateway Time-out', 
215     505  => 'HTTP Version not supported'
216   }.freeze