Add rb_thread_schedule() and threaded_processing option
[ebb.git] / ruby_lib / ebb.rb
blob884c54326b329c4bbf8b89cb6599207a445a1e4b
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 module Ebb
5   LIBDIR = File.dirname(__FILE__)
6   VERSION = File.read(LIBDIR + "/../VERSION").gsub(/\s/,'')
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     
22     puts "Ebb listening at http://0.0.0.0:#{port}/ (#{threaded_processing ? 'threaded' : 'sequential'} processing)"
23     trap('INT')  { @running = false }
24     @running = true
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| c.process(app) }
31         else
32           client.process(app)
33         end
34       end
35     end
36     
37     puts "Ebb unlistening"
38     FFI::server_unlisten()
39   end
40   
41   def FFI.waiting_clients
42     @waiting_clients
43   end
44   
45   class Client
46     BASE_ENV = {
47       'SERVER_NAME' => '0.0.0.0',
48       'SCRIPT_NAME' => '',
49       'SERVER_SOFTWARE' => "Ebb #{Ebb::VERSION}",
50       'SERVER_PROTOCOL' => 'HTTP/1.1',
51       'rack.version' => [0, 1],
52       'rack.errors' => STDERR,
53       'rack.url_scheme' => 'http',
54       'rack.multithread'  => true,
55       'rack.multiprocess' => false,
56       'rack.run_once' => false
57     }
58     
59     def process(app)
60       #puts "Request: #{client.inspect}\n"
61       begin
62         status, headers, body = app.call(env)
63       rescue
64         raise if $DEBUG
65         status = 500
66         headers = {'Content-Type' => 'text/plain'}
67         body = "Internal Server Error\n"
68       end
69       
70       FFI::client_write_status(self, status.to_i, HTTP_STATUS_CODES[status.to_i])
71       
72       if body.respond_to? :length and status != 304
73         headers['Connection'] = 'close'
74         headers['Content-Length'] = body.length
75       end
76       
77       headers.each { |k, v| write_header(k,v) }
78       
79       write("\r\n")
80       
81       # Not many apps use streaming yet so i'll hold off on that feature
82       # until the rest of ebb is more developed.
83       if body.kind_of?(String)
84         write(body)
85         FFI::client_set_body_written(self, true)
86         FFI::client_begin_transmission(self)
87       else
88         FFI::client_begin_transmission(self)
89         body.each { |p| write(p) }
90         FFI::client_set_body_written(self, true)
91       end
92     ensure
93       FFI::client_release(self)
94     end
95     
96     private
97     
98     def env
99       env = FFI::client_env(self).update(BASE_ENV)
100       env['rack.input'] = RequestBody.new(self)
101       env
102     end
103     
104     def write(data)
105       FFI::client_write(self, data)
106     end
107     
108     def write_header(field, value)
109       FFI::client_write_header(self, field.to_s, value.to_s)
110     end
111   end
112   
113   class RequestBody
114     def initialize(client)
115       @client = client
116     end
117     
118     def read(len)
119       FFI::client_read_input(@client, len)
120     end
121     
122     def gets
123       raise NotImplementedError
124     end
125     
126     def each
127       raise NotImplementedError
128     end
129   end
130   
131   HTTP_STATUS_CODES = {  
132     100  => 'Continue', 
133     101  => 'Switching Protocols', 
134     200  => 'OK', 
135     201  => 'Created', 
136     202  => 'Accepted', 
137     203  => 'Non-Authoritative Information', 
138     204  => 'No Content', 
139     205  => 'Reset Content', 
140     206  => 'Partial Content', 
141     300  => 'Multiple Choices', 
142     301  => 'Moved Permanently', 
143     302  => 'Moved Temporarily', 
144     303  => 'See Other', 
145     304  => 'Not Modified', 
146     305  => 'Use Proxy', 
147     400  => 'Bad Request', 
148     401  => 'Unauthorized', 
149     402  => 'Payment Required', 
150     403  => 'Forbidden', 
151     404  => 'Not Found', 
152     405  => 'Method Not Allowed', 
153     406  => 'Not Acceptable', 
154     407  => 'Proxy Authentication Required', 
155     408  => 'Request Time-out', 
156     409  => 'Conflict', 
157     410  => 'Gone', 
158     411  => 'Length Required', 
159     412  => 'Precondition Failed', 
160     413  => 'Request Entity Too Large', 
161     414  => 'Request-URI Too Large', 
162     415  => 'Unsupported Media Type', 
163     500  => 'Internal Server Error', 
164     501  => 'Not Implemented', 
165     502  => 'Bad Gateway', 
166     503  => 'Service Unavailable', 
167     504  => 'Gateway Time-out', 
168     505  => 'HTTP Version not supported'
169   }.freeze