ebb 0.0.4 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|