ebb 0.0.4 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README +53 -62
- data/benchmark/application.rb +3 -9
- data/benchmark/bench_results.rb +30 -17
- data/benchmark/server_test.rb +54 -12
- data/bin/ebb_rails +1 -76
- data/ruby_lib/ebb.rb +136 -107
- data/ruby_lib/ebb/runner.rb +135 -0
- data/ruby_lib/ebb/runner/rails.rb +34 -0
- data/ruby_lib/rack/adapter/rails.rb +2 -0
- data/src/ebb.c +174 -142
- data/src/ebb.h +14 -15
- data/src/ebb_ruby.c +120 -85
- data/src/parser.c +322 -410
- data/test/basic_test.rb +6 -170
- data/test/ebb_rails_test.rb +34 -0
- data/test/env_test.rb +8 -12
- data/test/helper.rb +86 -0
- metadata +21 -11
- data/VERSION +0 -1
- data/ruby_lib/daemonizable.rb +0 -98
- data/test/echo_server.rb +0 -16
- data/test/test.py +0 -15
data/ruby_lib/ebb.rb
CHANGED
@@ -1,50 +1,142 @@
|
|
1
|
-
#
|
2
|
-
# Copyright (c)
|
3
|
-
#
|
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'
|
4
5
|
module Ebb
|
5
6
|
LIBDIR = File.dirname(__FILE__)
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
+
|
13
96
|
class Client
|
14
97
|
BASE_ENV = {
|
98
|
+
'SERVER_NAME' => '0.0.0.0',
|
15
99
|
'SCRIPT_NAME' => '',
|
16
100
|
'SERVER_SOFTWARE' => "Ebb #{Ebb::VERSION}",
|
17
101
|
'SERVER_PROTOCOL' => 'HTTP/1.1',
|
18
102
|
'rack.version' => [0, 1],
|
19
103
|
'rack.errors' => STDERR,
|
20
104
|
'rack.url_scheme' => 'http',
|
21
|
-
'rack.multithread' => false,
|
22
105
|
'rack.multiprocess' => false,
|
23
106
|
'rack.run_once' => false
|
24
|
-
}
|
107
|
+
}
|
25
108
|
|
26
109
|
def env
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
env
|
31
|
-
end
|
110
|
+
env = FFI::client_env(self).update(BASE_ENV)
|
111
|
+
env['rack.input'] = RequestBody.new(self)
|
112
|
+
env
|
32
113
|
end
|
33
114
|
|
34
|
-
def
|
35
|
-
|
115
|
+
def write_status(status)
|
116
|
+
s = status.to_i
|
117
|
+
FFI::client_write_status(self, s, HTTP_STATUS_CODES[s])
|
36
118
|
end
|
37
119
|
|
38
120
|
def write(data)
|
39
121
|
FFI::client_write(self, data)
|
40
122
|
end
|
41
123
|
|
42
|
-
def
|
43
|
-
|
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
|
44
128
|
end
|
45
129
|
|
46
|
-
def
|
47
|
-
FFI::
|
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)
|
48
140
|
end
|
49
141
|
end
|
50
142
|
|
@@ -53,99 +145,36 @@ module Ebb
|
|
53
145
|
@client = client
|
54
146
|
end
|
55
147
|
|
56
|
-
def read(len)
|
57
|
-
|
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
|
58
162
|
end
|
59
163
|
|
60
164
|
def gets
|
61
|
-
|
165
|
+
io.gets
|
62
166
|
end
|
63
167
|
|
64
|
-
def each
|
65
|
-
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
class Server
|
70
|
-
include Daemonizable
|
71
|
-
def self.run(app, options={})
|
72
|
-
# port must be an integer
|
73
|
-
server = self.new(app, options)
|
74
|
-
yield server if block_given?
|
75
|
-
server.start
|
76
|
-
end
|
77
|
-
|
78
|
-
def initialize(app, options={})
|
79
|
-
@socket = options[:socket]
|
80
|
-
@port = (options[:port] || 4001).to_i
|
81
|
-
@timeout = options[:timeout]
|
82
|
-
@app = app
|
83
|
-
end
|
84
|
-
|
85
|
-
def start
|
86
|
-
trap('INT') { @running = false }
|
87
|
-
|
88
|
-
if @socket
|
89
|
-
raise NotImplemented
|
90
|
-
FFI::server_listen_on_socket(self, @socket) or raise "Problem listening on socket #{@socket}"
|
91
|
-
else
|
92
|
-
FFI::server_listen_on_port(self, @port) or raise "Problem listening on port #{@port}"
|
93
|
-
end
|
94
|
-
@waiting_clients = []
|
95
|
-
|
96
|
-
puts "Ebb listening at http://0.0.0.0:#{@port}/"
|
97
|
-
|
98
|
-
@running = true
|
99
|
-
while FFI::server_process_connections(self) and @running
|
100
|
-
unless @waiting_clients.empty?
|
101
|
-
if $DEBUG and @waiting_clients.length > 1
|
102
|
-
puts "#{@waiting_clients.length} waiting clients"
|
103
|
-
end
|
104
|
-
client = @waiting_clients.shift
|
105
|
-
process_client(client)
|
106
|
-
end
|
107
|
-
end
|
108
|
-
puts "Ebb unlistening"
|
109
|
-
FFI::server_unlisten(self)
|
110
|
-
end
|
111
|
-
|
112
|
-
def process_client(client)
|
113
|
-
#puts "Request: #{client.env.inspect}\n"
|
114
|
-
begin
|
115
|
-
status, headers, body = @app.call(client.env)
|
116
|
-
rescue
|
117
|
-
raise if $DEBUG
|
118
|
-
status = 500
|
119
|
-
headers = {'Content-Type' => 'text/plain'}
|
120
|
-
body = "Internal Server Error\n"
|
121
|
-
end
|
122
|
-
|
123
|
-
client.write_status(status)
|
124
|
-
|
125
|
-
if body.respond_to? :length and status != 304
|
126
|
-
headers['Connection'] = 'close'
|
127
|
-
headers['Content-Length'] = body.length
|
128
|
-
end
|
129
|
-
|
130
|
-
headers.each { |k, v| client.write_header(k,v) }
|
131
|
-
|
132
|
-
client.write "\r\n"
|
133
|
-
|
134
|
-
# Not many apps use streaming yet so i'll hold off on that feature
|
135
|
-
# until the rest of ebb is more developed.
|
136
|
-
if body.kind_of?(String)
|
137
|
-
client.write body
|
138
|
-
else
|
139
|
-
body.each { |p| client.write p }
|
140
|
-
end
|
141
|
-
client.finished
|
168
|
+
def each(&block)
|
169
|
+
io.each(&block)
|
142
170
|
end
|
143
171
|
|
144
|
-
def
|
145
|
-
|
172
|
+
def io
|
173
|
+
@io ||= StringIO.new(read)
|
146
174
|
end
|
147
175
|
end
|
148
176
|
|
177
|
+
|
149
178
|
HTTP_STATUS_CODES = {
|
150
179
|
100 => 'Continue',
|
151
180
|
101 => 'Switching Protocols',
|
@@ -185,4 +214,4 @@ module Ebb
|
|
185
214
|
504 => 'Gateway Time-out',
|
186
215
|
505 => 'HTTP Version not supported'
|
187
216
|
}.freeze
|
188
|
-
end
|
217
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
module Kernel
|
4
|
+
unless respond_to? :daemonize # Already part of Ruby 1.9, yeah!
|
5
|
+
# Turns the current script into a daemon process that detaches from the console.
|
6
|
+
# It can be shut down with a TERM signal. Taken from ActiveSupport.
|
7
|
+
def daemonize
|
8
|
+
exit if fork # Parent exits, child continues.
|
9
|
+
Process.setsid # Become session leader.
|
10
|
+
exit if fork # Zap session leader. See [1].
|
11
|
+
Dir.chdir "/" # Release old working directory.
|
12
|
+
File.umask 0000 # Ensure sensible umask. Adjust as needed.
|
13
|
+
STDIN.reopen "/dev/null" # Free file descriptors and
|
14
|
+
STDOUT.reopen "/dev/null", "a" # point them somewhere sensible.
|
15
|
+
STDERR.reopen STDOUT # STDOUT/ERR should better go to a logfile.
|
16
|
+
trap("TERM") { exit }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module Ebb
|
22
|
+
class Runner
|
23
|
+
# Classes are modules and I hate this 'Base' class pattern. I'm putting
|
24
|
+
# other classes inside this one.
|
25
|
+
autoload :Rails, LIBDIR + '/ebb/runner/rails'
|
26
|
+
|
27
|
+
# Kill the process which PID is stored in +pid_file+.
|
28
|
+
def self.kill(pid_file, timeout=60)
|
29
|
+
raise ArgumentError, 'You must specify a pid_file to stop deamonized server' unless pid_file
|
30
|
+
|
31
|
+
if pid = File.read(pid_file)
|
32
|
+
pid = pid.to_i
|
33
|
+
|
34
|
+
Process.kill('KILL', pid)
|
35
|
+
Ebb.log.puts "stopped!"
|
36
|
+
else
|
37
|
+
Ebb.log.puts "Can't stop process, no PID found in #{@pid_file}"
|
38
|
+
end
|
39
|
+
rescue Errno::ESRCH # No such process
|
40
|
+
Ebb.log.puts "process not found!"
|
41
|
+
ensure
|
42
|
+
File.delete(pid_file) rescue nil
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.remove_pid_file(file)
|
46
|
+
File.delete(file) if file && File.exists?(file) && Process.pid == File.read(file)
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.write_pid_file(file)
|
50
|
+
Ebb.log.puts ">> Writing PID to #{file}"
|
51
|
+
open(file,"w+") { |f| f.write(Process.pid) }
|
52
|
+
File.chmod(0644, file)
|
53
|
+
end
|
54
|
+
|
55
|
+
attr_reader :options
|
56
|
+
|
57
|
+
def initialize
|
58
|
+
@parser = OptionParser.new
|
59
|
+
@options = {
|
60
|
+
:port => 4001,
|
61
|
+
:timeout => 60,
|
62
|
+
:threaded_processing => true
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
def parse_options(argv)
|
67
|
+
@parser.banner = "Usage: #{self.class} [options] start | stop"
|
68
|
+
@parser.separator ""
|
69
|
+
extra_options if respond_to?(:extra_options)
|
70
|
+
|
71
|
+
@parser.separator ""
|
72
|
+
# opts.on("-s", "--socket SOCKET", "listen on socket") { |socket| options[:socket] = socket }
|
73
|
+
@parser.on("-p", "--port PORT", "(default: #{@options[:port]})") { |p| @options[:port]=p }
|
74
|
+
@parser.on("-d", "--daemonize", "Daemonize") { @options[:daemonize] = true }
|
75
|
+
@parser.on("-l", "--log-file FILE", "File to redirect output") { |f| @options[:log_file]=f }
|
76
|
+
@parser.on("-P", "--pid-file FILE", "File to store PID") { |f| @options[:pid_file]=f }
|
77
|
+
# @parser.on("-t", "--timeout SECONDS", "(default: #{@options[:timeout]})") { |s| @options[:timeout]=s }
|
78
|
+
|
79
|
+
@parser.separator ""
|
80
|
+
@parser.on_tail("-h", "--help", "Show this message") do
|
81
|
+
Ebb.log.puts @parser
|
82
|
+
exit
|
83
|
+
end
|
84
|
+
@parser.on_tail('-v', '--version', "Show version") do
|
85
|
+
Ebb.log.puts "Ebb #{Ebb::VERSION}"
|
86
|
+
exit
|
87
|
+
end
|
88
|
+
|
89
|
+
@parser.parse!(argv)
|
90
|
+
end
|
91
|
+
|
92
|
+
def run(argv)
|
93
|
+
parse_options(argv)
|
94
|
+
|
95
|
+
case argv[0]
|
96
|
+
when 'start'
|
97
|
+
Ebb.log.print("Ebb is loading the application...")
|
98
|
+
Ebb.log.flush()
|
99
|
+
@app = app(@options)
|
100
|
+
Ebb.log.puts("done")
|
101
|
+
|
102
|
+
if @options[:daemonize]
|
103
|
+
pwd = Dir.pwd # Current directory is changed during daemonization, so store it
|
104
|
+
Kernel.daemonize
|
105
|
+
Dir.chdir pwd
|
106
|
+
trap('HUP', 'IGNORE') # Don't die upon logout
|
107
|
+
end
|
108
|
+
|
109
|
+
if @options[:log_file]
|
110
|
+
[STDOUT, STDERR].each { |f| f.reopen @options[:log_file], 'a' }
|
111
|
+
end
|
112
|
+
|
113
|
+
if @options[:pid_file]
|
114
|
+
Runner.write_pid_file(@options[:pid_file])
|
115
|
+
at_exit do
|
116
|
+
Ebb.log.puts ">> Exiting!"
|
117
|
+
Runner.remove_pid_file(@options[:pid_file])
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
Ebb::start_server(@app, @options)
|
122
|
+
when 'stop'
|
123
|
+
Ebb::Runner.kill @options[:pid_file], @options[:timeout]
|
124
|
+
when nil
|
125
|
+
Ebb.log.puts "Command required"
|
126
|
+
Ebb.log.puts @parser
|
127
|
+
exit 1
|
128
|
+
else
|
129
|
+
abort "Invalid command : #{argv[0]}"
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Rack
|
2
|
+
module Adapter
|
3
|
+
autoload :Rails, Ebb::LIBDIR + '/rack/adapter/rails'
|
4
|
+
end
|
5
|
+
end
|
6
|
+
|
7
|
+
module Ebb
|
8
|
+
class Runner
|
9
|
+
class Rails < Runner
|
10
|
+
def extra_options
|
11
|
+
# defaults for ebb_rails
|
12
|
+
@options.update(
|
13
|
+
:environment => 'development',
|
14
|
+
:port => 3000,
|
15
|
+
# rails has a mutex lock around each request - threaded processing
|
16
|
+
# will only slow things down
|
17
|
+
:threaded_processing => false
|
18
|
+
)
|
19
|
+
|
20
|
+
@parser.on("-e", "--env ENV",
|
21
|
+
"Rails environment (default: development)") do |env|
|
22
|
+
@options[:environment] = env
|
23
|
+
end
|
24
|
+
@parser.on("-c", "--chdir DIR", "RAILS_ROOT directory") do |c|
|
25
|
+
@options[:root] = c
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def app(options)
|
30
|
+
Rack::Adapter::Rails.new(options)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|