ebb 0.0.4 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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