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.
@@ -1,50 +1,142 @@
1
- # A ruby binding to the ebb web server
2
- # Copyright (c) 2007 Ry Dahl <ry.d4hl@gmail.com>
3
- # This software is released under the "MIT License". See README file for details.
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
- VERSION = File.read(LIBDIR + "/../VERSION").gsub(/\s/,'')
7
- end
8
-
9
- require Ebb::LIBDIR + '/../src/ebb_ext'
10
- require Ebb::LIBDIR + '/daemonizable'
11
-
12
- module Ebb
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
- }.freeze
107
+ }
25
108
 
26
109
  def env
27
- @env ||= begin
28
- env = FFI::client_env(self).update(BASE_ENV)
29
- env['rack.input'] = RequestBody.new(self)
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 finished
35
- FFI::client_finished(self)
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 write_status(status)
43
- FFI::client_write_status(self, status.to_i, HTTP_STATUS_CODES[status])
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 write_header(field, value)
47
- FFI::client_write_header(self, field.to_s, value.to_s)
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
- FFI::client_read_input(@client, len)
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
- raise NotImplementedError
165
+ io.gets
62
166
  end
63
167
 
64
- def each
65
- raise NotImplementedError
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 log(msg)
145
- puts msg
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