unicorn 0.2.3 → 0.4.1
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/.document +1 -1
- data/.gitignore +1 -0
- data/CHANGELOG +1 -0
- data/DESIGN +4 -0
- data/GNUmakefile +30 -6
- data/Manifest +62 -3
- data/README +52 -42
- data/SIGNALS +17 -17
- data/TODO +27 -5
- data/bin/unicorn +15 -13
- data/bin/unicorn_rails +59 -22
- data/ext/unicorn/http11/http11.c +25 -104
- data/ext/unicorn/http11/http11_parser.c +24 -23
- data/ext/unicorn/http11/http11_parser.h +1 -3
- data/ext/unicorn/http11/http11_parser.rl +2 -1
- data/lib/unicorn.rb +58 -44
- data/lib/unicorn/app/old_rails.rb +23 -0
- data/lib/unicorn/app/old_rails/static.rb +58 -0
- data/lib/unicorn/cgi_wrapper.rb +151 -0
- data/lib/unicorn/configurator.rb +71 -31
- data/lib/unicorn/const.rb +9 -34
- data/lib/unicorn/http_request.rb +63 -66
- data/lib/unicorn/http_response.rb +6 -1
- data/lib/unicorn/socket.rb +15 -2
- data/test/benchmark/README +55 -0
- data/test/benchmark/big_request.rb +35 -0
- data/test/benchmark/dd.ru +18 -0
- data/test/benchmark/request.rb +47 -0
- data/test/benchmark/response.rb +29 -0
- data/test/exec/test_exec.rb +41 -157
- data/test/rails/app-1.2.3/.gitignore +2 -0
- data/test/rails/app-1.2.3/app/controllers/application.rb +4 -0
- data/test/rails/app-1.2.3/app/controllers/foo_controller.rb +34 -0
- data/test/rails/app-1.2.3/app/helpers/application_helper.rb +2 -0
- data/test/rails/app-1.2.3/config/boot.rb +9 -0
- data/test/rails/app-1.2.3/config/database.yml +12 -0
- data/test/rails/app-1.2.3/config/environment.rb +10 -0
- data/test/rails/app-1.2.3/config/environments/development.rb +7 -0
- data/test/rails/app-1.2.3/config/environments/production.rb +3 -0
- data/test/rails/app-1.2.3/config/routes.rb +4 -0
- data/test/rails/app-1.2.3/db/.gitignore +0 -0
- data/test/rails/app-1.2.3/public/404.html +1 -0
- data/test/rails/app-1.2.3/public/500.html +1 -0
- data/test/rails/app-2.0.2/.gitignore +2 -0
- data/test/rails/app-2.0.2/app/controllers/application.rb +2 -0
- data/test/rails/app-2.0.2/app/controllers/foo_controller.rb +34 -0
- data/test/rails/app-2.0.2/app/helpers/application_helper.rb +2 -0
- data/test/rails/app-2.0.2/config/boot.rb +9 -0
- data/test/rails/app-2.0.2/config/database.yml +12 -0
- data/test/rails/app-2.0.2/config/environment.rb +14 -0
- data/test/rails/app-2.0.2/config/environments/development.rb +6 -0
- data/test/rails/app-2.0.2/config/environments/production.rb +3 -0
- data/test/rails/app-2.0.2/config/routes.rb +4 -0
- data/test/rails/app-2.0.2/db/.gitignore +0 -0
- data/test/rails/app-2.0.2/public/404.html +1 -0
- data/test/rails/app-2.0.2/public/500.html +1 -0
- data/test/rails/app-2.2.2/.gitignore +2 -0
- data/test/rails/app-2.2.2/app/controllers/application.rb +2 -0
- data/test/rails/app-2.2.2/app/controllers/foo_controller.rb +34 -0
- data/test/rails/app-2.2.2/app/helpers/application_helper.rb +2 -0
- data/test/rails/app-2.2.2/config/boot.rb +109 -0
- data/test/rails/app-2.2.2/config/database.yml +12 -0
- data/test/rails/app-2.2.2/config/environment.rb +14 -0
- data/test/rails/app-2.2.2/config/environments/development.rb +5 -0
- data/test/rails/app-2.2.2/config/environments/production.rb +3 -0
- data/test/rails/app-2.2.2/config/routes.rb +4 -0
- data/test/rails/app-2.2.2/db/.gitignore +0 -0
- data/test/rails/app-2.2.2/public/404.html +1 -0
- data/test/rails/app-2.2.2/public/500.html +1 -0
- data/test/rails/app-2.3.2.1/.gitignore +2 -0
- data/test/rails/app-2.3.2.1/app/controllers/application_controller.rb +3 -0
- data/test/rails/app-2.3.2.1/app/controllers/foo_controller.rb +34 -0
- data/test/rails/app-2.3.2.1/app/helpers/application_helper.rb +2 -0
- data/test/rails/app-2.3.2.1/config/boot.rb +107 -0
- data/test/rails/app-2.3.2.1/config/database.yml +12 -0
- data/test/rails/app-2.3.2.1/config/environment.rb +14 -0
- data/test/rails/app-2.3.2.1/config/environments/development.rb +5 -0
- data/test/rails/app-2.3.2.1/config/environments/production.rb +4 -0
- data/test/rails/app-2.3.2.1/config/routes.rb +4 -0
- data/test/rails/app-2.3.2.1/db/.gitignore +0 -0
- data/test/rails/app-2.3.2.1/public/404.html +1 -0
- data/test/rails/app-2.3.2.1/public/500.html +1 -0
- data/test/rails/test_rails.rb +243 -0
- data/test/test_helper.rb +149 -2
- data/test/unit/test_configurator.rb +46 -0
- data/test/unit/test_http_parser.rb +77 -36
- data/test/unit/test_request.rb +2 -0
- data/test/unit/test_response.rb +20 -4
- data/test/unit/test_server.rb +30 -1
- data/test/unit/test_socket_helper.rb +159 -0
- data/unicorn.gemspec +5 -5
- metadata +68 -5
- data/test/benchmark/previous.rb +0 -11
- data/test/benchmark/simple.rb +0 -11
- data/test/benchmark/utils.rb +0 -82
|
@@ -36,10 +36,8 @@ typedef struct http_parser {
|
|
|
36
36
|
|
|
37
37
|
int http_parser_init(http_parser *parser);
|
|
38
38
|
int http_parser_finish(http_parser *parser);
|
|
39
|
-
size_t http_parser_execute(http_parser *parser, const char *data, size_t len
|
|
39
|
+
size_t http_parser_execute(http_parser *parser, const char *data, size_t len);
|
|
40
40
|
int http_parser_has_error(http_parser *parser);
|
|
41
41
|
int http_parser_is_finished(http_parser *parser);
|
|
42
42
|
|
|
43
|
-
#define http_parser_nread(parser) (parser)->nread
|
|
44
|
-
|
|
45
43
|
#endif
|
|
@@ -105,9 +105,10 @@ int http_parser_init(http_parser *parser) {
|
|
|
105
105
|
|
|
106
106
|
|
|
107
107
|
/** exec **/
|
|
108
|
-
size_t http_parser_execute(http_parser *parser, const char *buffer, size_t len
|
|
108
|
+
size_t http_parser_execute(http_parser *parser, const char *buffer, size_t len) {
|
|
109
109
|
const char *p, *pe;
|
|
110
110
|
int cs = parser->cs;
|
|
111
|
+
size_t off = parser->nread;
|
|
111
112
|
|
|
112
113
|
assert(off <= len && "offset past end of buffer");
|
|
113
114
|
|
data/lib/unicorn.rb
CHANGED
|
@@ -59,6 +59,7 @@ module Unicorn
|
|
|
59
59
|
@request = @rd_sig = @wr_sig = nil
|
|
60
60
|
@reexec_pid = 0
|
|
61
61
|
@config = Configurator.new(options.merge(:use_defaults => true))
|
|
62
|
+
@listener_opts = {}
|
|
62
63
|
@config.commit!(self, :skip => [:listeners, :pid])
|
|
63
64
|
@listeners = []
|
|
64
65
|
end
|
|
@@ -85,6 +86,9 @@ module Unicorn
|
|
|
85
86
|
# share the same OS-level file descriptor as the higher-level *Server
|
|
86
87
|
# objects; we need to prevent Socket objects from being garbage-collected
|
|
87
88
|
config_listeners -= listener_names
|
|
89
|
+
if config_listeners.empty? && @listeners.empty?
|
|
90
|
+
config_listeners << Unicorn::Const::DEFAULT_LISTEN
|
|
91
|
+
end
|
|
88
92
|
config_listeners.each { |addr| listen(addr) }
|
|
89
93
|
raise ArgumentError, "no listeners" if @listeners.empty?
|
|
90
94
|
self.pid = @config[:pid]
|
|
@@ -123,19 +127,19 @@ module Unicorn
|
|
|
123
127
|
raise ArgumentError, "Already running on PID:#{x} " \
|
|
124
128
|
"(or pid=#{path} is stale)"
|
|
125
129
|
end
|
|
126
|
-
File.open(path, 'wb') { |fp| fp.syswrite("#{$$}\n") }
|
|
127
130
|
end
|
|
128
|
-
unlink_pid_safe(@pid) if @pid
|
|
131
|
+
unlink_pid_safe(@pid) if @pid
|
|
132
|
+
File.open(path, 'wb') { |fp| fp.syswrite("#$$\n") } if path
|
|
129
133
|
@pid = path
|
|
130
134
|
end
|
|
131
135
|
|
|
132
136
|
# add a given address to the +listeners+ set, idempotently
|
|
133
137
|
# Allows workers to add a private, per-process listener via the
|
|
134
138
|
# @after_fork hook. Very useful for debugging and testing.
|
|
135
|
-
def listen(address)
|
|
139
|
+
def listen(address, opt = {}.merge(@listener_opts[address] || {}))
|
|
136
140
|
return if String === address && listener_names.include?(address)
|
|
137
141
|
|
|
138
|
-
if io = bind_listen(address,
|
|
142
|
+
if io = bind_listen(address, opt)
|
|
139
143
|
if Socket == io.class
|
|
140
144
|
@io_purgatory << io
|
|
141
145
|
io = server_cast(io)
|
|
@@ -157,12 +161,11 @@ module Unicorn
|
|
|
157
161
|
# this pipe is used to wake us up from select(2) in #join when signals
|
|
158
162
|
# are trapped. See trap_deferred
|
|
159
163
|
@rd_sig, @wr_sig = IO.pipe unless (@rd_sig && @wr_sig)
|
|
160
|
-
@rd_sig.nonblock = @wr_sig.nonblock = true
|
|
161
164
|
mode = nil
|
|
162
165
|
respawn = true
|
|
163
166
|
|
|
164
167
|
QUEUE_SIGS.each { |sig| trap_deferred(sig) }
|
|
165
|
-
trap(
|
|
168
|
+
trap(:CHLD) { |sig_nr| awaken_master }
|
|
166
169
|
$0 = "unicorn master"
|
|
167
170
|
logger.info "master process ready" # test_exec.rb relies on this message
|
|
168
171
|
begin
|
|
@@ -173,27 +176,27 @@ module Unicorn
|
|
|
173
176
|
murder_lazy_workers
|
|
174
177
|
spawn_missing_workers if respawn
|
|
175
178
|
master_sleep
|
|
176
|
-
when
|
|
179
|
+
when :QUIT # graceful shutdown
|
|
177
180
|
break
|
|
178
|
-
when
|
|
181
|
+
when :TERM, :INT # immediate shutdown
|
|
179
182
|
stop(false)
|
|
180
183
|
break
|
|
181
|
-
when
|
|
184
|
+
when :USR1 # rotate logs
|
|
182
185
|
logger.info "master rotating logs..."
|
|
183
186
|
Unicorn::Util.reopen_logs
|
|
184
187
|
logger.info "master done rotating logs"
|
|
185
|
-
kill_each_worker(
|
|
186
|
-
when
|
|
188
|
+
kill_each_worker(:USR1)
|
|
189
|
+
when :USR2 # exec binary, stay alive in case something went wrong
|
|
187
190
|
reexec
|
|
188
|
-
when
|
|
191
|
+
when :WINCH
|
|
189
192
|
if Process.ppid == 1 || Process.getpgrp != $$
|
|
190
193
|
respawn = false
|
|
191
194
|
logger.info "gracefully stopping all workers"
|
|
192
|
-
kill_each_worker(
|
|
195
|
+
kill_each_worker(:QUIT)
|
|
193
196
|
else
|
|
194
197
|
logger.info "SIGWINCH ignored because we're not daemonized"
|
|
195
198
|
end
|
|
196
|
-
when
|
|
199
|
+
when :HUP
|
|
197
200
|
respawn = true
|
|
198
201
|
if @config.config_file
|
|
199
202
|
load_config!
|
|
@@ -221,7 +224,7 @@ module Unicorn
|
|
|
221
224
|
|
|
222
225
|
# Terminates all workers, but does not exit master process
|
|
223
226
|
def stop(graceful = true)
|
|
224
|
-
kill_each_worker(graceful ?
|
|
227
|
+
kill_each_worker(graceful ? :QUIT : :TERM)
|
|
225
228
|
timeleft = @timeout
|
|
226
229
|
step = 0.2
|
|
227
230
|
reap_all_workers
|
|
@@ -229,7 +232,7 @@ module Unicorn
|
|
|
229
232
|
sleep(step)
|
|
230
233
|
reap_all_workers
|
|
231
234
|
(timeleft -= step) > 0 and next
|
|
232
|
-
kill_each_worker(
|
|
235
|
+
kill_each_worker(:KILL)
|
|
233
236
|
end
|
|
234
237
|
ensure
|
|
235
238
|
self.listeners = []
|
|
@@ -238,8 +241,7 @@ module Unicorn
|
|
|
238
241
|
private
|
|
239
242
|
|
|
240
243
|
# list of signals we care about and trap in master.
|
|
241
|
-
QUEUE_SIGS =
|
|
242
|
-
%w(WINCH QUIT INT TERM USR1 USR2 HUP).map { |x| x.freeze }.freeze
|
|
244
|
+
QUEUE_SIGS = [ :WINCH, :QUIT, :INT, :TERM, :USR1, :USR2, :HUP ].freeze
|
|
243
245
|
|
|
244
246
|
# defer a signal for later processing in #join (master process)
|
|
245
247
|
def trap_deferred(signal)
|
|
@@ -259,16 +261,16 @@ module Unicorn
|
|
|
259
261
|
begin
|
|
260
262
|
ready = IO.select([@rd_sig], nil, nil, 1)
|
|
261
263
|
ready && ready[0] && ready[0][0] or return
|
|
262
|
-
loop { @rd_sig.
|
|
264
|
+
loop { @rd_sig.read_nonblock(Const::CHUNK_SIZE) }
|
|
263
265
|
rescue Errno::EAGAIN, Errno::EINTR
|
|
264
266
|
end
|
|
265
267
|
end
|
|
266
268
|
|
|
267
269
|
def awaken_master
|
|
268
270
|
begin
|
|
269
|
-
@wr_sig.
|
|
270
|
-
rescue Errno::EAGAIN
|
|
271
|
-
|
|
271
|
+
@wr_sig.write_nonblock('.') # wakeup master process from IO.select
|
|
272
|
+
rescue Errno::EAGAIN, Errno::EINTR
|
|
273
|
+
# pipe is full, master should wake up anyways
|
|
272
274
|
retry
|
|
273
275
|
end
|
|
274
276
|
end
|
|
@@ -350,7 +352,7 @@ module Unicorn
|
|
|
350
352
|
@workers.each_pair do |pid, worker|
|
|
351
353
|
(now - worker.tempfile.ctime) <= @timeout and next
|
|
352
354
|
logger.error "worker=#{worker.nr} PID:#{pid} is too old, killing"
|
|
353
|
-
kill_worker(
|
|
355
|
+
kill_worker(:KILL, pid) # take no prisoners for @timeout violations
|
|
354
356
|
worker.tempfile.close rescue nil
|
|
355
357
|
end
|
|
356
358
|
end
|
|
@@ -363,7 +365,7 @@ module Unicorn
|
|
|
363
365
|
Dir.chdir(@start_ctx[:cwd])
|
|
364
366
|
rescue Errno::ENOENT => err
|
|
365
367
|
logger.fatal "#{err.inspect} (#{@start_ctx[:cwd]})"
|
|
366
|
-
@sig_queue <<
|
|
368
|
+
@sig_queue << :QUIT # forcibly emulate SIGQUIT
|
|
367
369
|
return
|
|
368
370
|
end
|
|
369
371
|
tempfile = Tempfile.new('') # as short as possible to save dir space
|
|
@@ -379,12 +381,21 @@ module Unicorn
|
|
|
379
381
|
# once a client is accepted, it is processed in its entirety here
|
|
380
382
|
# in 3 easy steps: read request, call app, write app response
|
|
381
383
|
def process_client(client)
|
|
382
|
-
|
|
384
|
+
client.nonblock = false
|
|
385
|
+
set_client_sockopt(client) if TCPSocket === client
|
|
386
|
+
env = @request.read(client)
|
|
383
387
|
app_response = @app.call(env)
|
|
384
388
|
HttpResponse.write(client, app_response)
|
|
389
|
+
# if we get any error, try to write something back to the client
|
|
390
|
+
# assuming we haven't closed the socket, but don't get hung up
|
|
391
|
+
# if the socket is already closed or broken. We'll always ensure
|
|
392
|
+
# the socket is closed at the end of this function
|
|
385
393
|
rescue EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL,Errno::EBADF
|
|
386
|
-
client.
|
|
394
|
+
client.write_nonblock(Const::ERROR_500_RESPONSE) rescue nil
|
|
395
|
+
rescue HttpParserError # try to tell the client they're bad
|
|
396
|
+
client.write_nonblock(Const::ERROR_400_RESPONSE) rescue nil
|
|
387
397
|
rescue Object => e
|
|
398
|
+
client.write_nonblock(Const::ERROR_500_RESPONSE) rescue nil
|
|
388
399
|
logger.error "Read error: #{e.inspect}"
|
|
389
400
|
logger.error e.backtrace.join("\n")
|
|
390
401
|
ensure
|
|
@@ -405,12 +416,7 @@ module Unicorn
|
|
|
405
416
|
build_app! unless @preload_app
|
|
406
417
|
@sig_queue.clear
|
|
407
418
|
QUEUE_SIGS.each { |sig| trap(sig, 'IGNORE') }
|
|
408
|
-
trap(
|
|
409
|
-
trap('USR1') do
|
|
410
|
-
@logger.info "worker=#{worker.nr} rotating logs..."
|
|
411
|
-
Unicorn::Util.reopen_logs
|
|
412
|
-
@logger.info "worker=#{worker.nr} done rotating logs"
|
|
413
|
-
end
|
|
419
|
+
trap(:CHLD, 'DEFAULT')
|
|
414
420
|
|
|
415
421
|
$0 = "unicorn worker[#{worker.nr}]"
|
|
416
422
|
@rd_sig.close if @rd_sig
|
|
@@ -435,13 +441,24 @@ module Unicorn
|
|
|
435
441
|
alive = true
|
|
436
442
|
ready = @listeners
|
|
437
443
|
client = nil
|
|
438
|
-
|
|
439
|
-
trap(
|
|
444
|
+
[:TERM, :INT].each { |sig| trap(sig) { exit(0) } } # instant shutdown
|
|
445
|
+
trap(:QUIT) do
|
|
440
446
|
alive = false
|
|
441
447
|
@listeners.each { |sock| sock.close rescue nil } # break IO.select
|
|
442
448
|
end
|
|
449
|
+
reopen_logs, (rd, wr) = false, IO.pipe
|
|
450
|
+
trap(:USR1) { reopen_logs = true; rd.close rescue nil } # break IO.select
|
|
451
|
+
@logger.info "worker=#{worker.nr} ready"
|
|
443
452
|
|
|
444
453
|
while alive && @master_pid == Process.ppid
|
|
454
|
+
if reopen_logs
|
|
455
|
+
reopen_logs = false
|
|
456
|
+
@logger.info "worker=#{worker.nr} rotating logs..."
|
|
457
|
+
Unicorn::Util.reopen_logs
|
|
458
|
+
@logger.info "worker=#{worker.nr} done rotating logs"
|
|
459
|
+
wr.close rescue nil
|
|
460
|
+
rd, wr = IO.pipe
|
|
461
|
+
end
|
|
445
462
|
# we're a goner in @timeout seconds anyways if tempfile.chmod
|
|
446
463
|
# breaks, so don't trap the exception. Using fchmod() since
|
|
447
464
|
# futimes() is not available in base Ruby and I very strongly
|
|
@@ -460,17 +477,14 @@ module Unicorn
|
|
|
460
477
|
rescue Errno::EAGAIN
|
|
461
478
|
next
|
|
462
479
|
end
|
|
463
|
-
accepted =
|
|
464
|
-
client.nonblock = false
|
|
465
|
-
set_client_sockopt(client) if TCPSocket === client
|
|
480
|
+
accepted = true
|
|
466
481
|
process_client(client)
|
|
467
482
|
rescue Errno::ECONNABORTED
|
|
468
483
|
# client closed the socket even before accept
|
|
469
|
-
|
|
470
|
-
client.close rescue nil
|
|
471
|
-
end
|
|
484
|
+
client.close rescue nil
|
|
472
485
|
end
|
|
473
486
|
tempfile.chmod(nr += 1)
|
|
487
|
+
break if reopen_logs
|
|
474
488
|
end
|
|
475
489
|
client = nil
|
|
476
490
|
|
|
@@ -478,18 +492,18 @@ module Unicorn
|
|
|
478
492
|
# we're probably reasonably busy, so avoid calling select(2)
|
|
479
493
|
# and try to do a blind non-blocking accept(2) on everything
|
|
480
494
|
# before we sleep again in select
|
|
481
|
-
if accepted
|
|
495
|
+
if accepted || reopen_logs
|
|
482
496
|
ready = @listeners
|
|
483
497
|
else
|
|
484
498
|
begin
|
|
485
499
|
tempfile.chmod(nr += 1)
|
|
486
500
|
# timeout used so we can detect parent death:
|
|
487
|
-
ret = IO.select(@listeners, nil,
|
|
501
|
+
ret = IO.select(@listeners, nil, [rd], @timeout/2.0) or next
|
|
488
502
|
ready = ret[0]
|
|
489
503
|
rescue Errno::EINTR
|
|
490
504
|
ready = @listeners
|
|
491
505
|
rescue Errno::EBADF => e
|
|
492
|
-
exit(alive ? 1 : 0)
|
|
506
|
+
reopen_logs or exit(alive ? 1 : 0)
|
|
493
507
|
end
|
|
494
508
|
end
|
|
495
509
|
rescue SystemExit => e
|
|
@@ -542,7 +556,7 @@ module Unicorn
|
|
|
542
556
|
logger.info "reloading config_file=#{@config.config_file}"
|
|
543
557
|
@config.reload
|
|
544
558
|
@config.commit!(self)
|
|
545
|
-
kill_each_worker(
|
|
559
|
+
kill_each_worker(:QUIT)
|
|
546
560
|
logger.info "done reloading config_file=#{@config.config_file}"
|
|
547
561
|
rescue Object => e
|
|
548
562
|
logger.error "error reloading config_file=#{@config.config_file}: " \
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# This code is based on the original Rails handler in Mongrel
|
|
2
|
+
# Copyright (c) 2005 Zed A. Shaw
|
|
3
|
+
# Copyright (c) 2009 Eric Wong
|
|
4
|
+
# You can redistribute it and/or modify it under the same terms as Ruby.
|
|
5
|
+
# Additional work donated by contributors. See CONTRIBUTORS for more info.
|
|
6
|
+
require 'unicorn/cgi_wrapper'
|
|
7
|
+
require 'dispatcher'
|
|
8
|
+
|
|
9
|
+
module Unicorn; module App; end; end
|
|
10
|
+
|
|
11
|
+
# Implements a handler that can run Rails.
|
|
12
|
+
class Unicorn::App::OldRails
|
|
13
|
+
|
|
14
|
+
def call(env)
|
|
15
|
+
cgi = Unicorn::CGIWrapper.new(env)
|
|
16
|
+
Dispatcher.dispatch(cgi,
|
|
17
|
+
ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS,
|
|
18
|
+
cgi.body)
|
|
19
|
+
cgi.out # finalize the response
|
|
20
|
+
cgi.rack_response
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# This code is based on the original Rails handler in Mongrel
|
|
2
|
+
# Copyright (c) 2005 Zed A. Shaw
|
|
3
|
+
# Copyright (c) 2009 Eric Wong
|
|
4
|
+
# You can redistribute it and/or modify it under the same terms as Ruby.
|
|
5
|
+
|
|
6
|
+
require 'rack/file'
|
|
7
|
+
|
|
8
|
+
# Static file handler for Rails < 2.3. This handler is only provided
|
|
9
|
+
# as a convenience for developers. Performance-minded deployments should
|
|
10
|
+
# use nginx (or similar) for serving static files.
|
|
11
|
+
#
|
|
12
|
+
# This supports page caching directly and will try to resolve a
|
|
13
|
+
# request in the following order:
|
|
14
|
+
#
|
|
15
|
+
# * If the requested exact PATH_INFO exists as a file then serve it.
|
|
16
|
+
# * If it exists at PATH_INFO+rest_operator+".html" exists
|
|
17
|
+
# then serve that.
|
|
18
|
+
#
|
|
19
|
+
# This means that if you are using page caching it will actually work
|
|
20
|
+
# with Unicorn and you should see a decent speed boost (but not as
|
|
21
|
+
# fast as if you use a static server like nginx).
|
|
22
|
+
class Unicorn::App::OldRails::Static
|
|
23
|
+
FILE_METHODS = { 'GET' => true, 'HEAD' => true }.freeze
|
|
24
|
+
REQUEST_METHOD = 'REQUEST_METHOD'.freeze
|
|
25
|
+
|
|
26
|
+
def initialize(app)
|
|
27
|
+
@app = app
|
|
28
|
+
@root = "#{::RAILS_ROOT}/public"
|
|
29
|
+
@file_server = ::Rack::File.new(@root)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def call(env)
|
|
33
|
+
# short circuit this ASAP if serving non-file methods
|
|
34
|
+
FILE_METHODS.include?(env[REQUEST_METHOD]) or return @app.call(env)
|
|
35
|
+
|
|
36
|
+
# first try the path as-is
|
|
37
|
+
path_info = env[Unicorn::Const::PATH_INFO].chomp("/")
|
|
38
|
+
if File.file?("#@root/#{::Rack::Utils.unescape(path_info)}")
|
|
39
|
+
# File exists as-is so serve it up
|
|
40
|
+
env[Unicorn::Const::PATH_INFO] = path_info
|
|
41
|
+
return @file_server.call(env)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# then try the cached version:
|
|
45
|
+
|
|
46
|
+
# grab the semi-colon REST operator used by old versions of Rails
|
|
47
|
+
# this is the reason we didn't just copy the new Rails::Rack::Static
|
|
48
|
+
env[Unicorn::Const::REQUEST_URI] =~ /^#{Regexp.escape(path_info)}(;[^\?]+)/
|
|
49
|
+
path_info << "#$1#{ActionController::Base.page_cache_extension}"
|
|
50
|
+
|
|
51
|
+
if File.file?("#@root/#{::Rack::Utils.unescape(path_info)}")
|
|
52
|
+
env[Unicorn::Const::PATH_INFO] = path_info
|
|
53
|
+
return @file_server.call(env)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
@app.call(env) # call OldRails
|
|
57
|
+
end
|
|
58
|
+
end if defined?(Unicorn::App::OldRails)
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# This code is based on the original CGIWrapper from Mongrel
|
|
2
|
+
# Copyright (c) 2005 Zed A. Shaw
|
|
3
|
+
# Copyright (c) 2009 Eric Wong
|
|
4
|
+
# You can redistribute it and/or modify it under the same terms as Ruby.
|
|
5
|
+
#
|
|
6
|
+
# Additional work donated by contributors. See CONTRIBUTORS for more info.
|
|
7
|
+
|
|
8
|
+
require 'cgi'
|
|
9
|
+
|
|
10
|
+
module Unicorn; end
|
|
11
|
+
|
|
12
|
+
# The beginning of a complete wrapper around Unicorn's internal HTTP
|
|
13
|
+
# processing system but maintaining the original Ruby CGI module. Use
|
|
14
|
+
# this only as a crutch to get existing CGI based systems working. It
|
|
15
|
+
# should handle everything, but please notify us if you see special
|
|
16
|
+
# warnings. This work is still very alpha so we need testers to help
|
|
17
|
+
# work out the various corner cases.
|
|
18
|
+
class Unicorn::CGIWrapper < ::CGI
|
|
19
|
+
undef_method :env_table
|
|
20
|
+
attr_reader :env_table
|
|
21
|
+
attr_reader :body
|
|
22
|
+
|
|
23
|
+
# these are stripped out of any keys passed to CGIWrapper.header function
|
|
24
|
+
NPH = 'nph'.freeze # Completely ignored, Unicorn outputs the date regardless
|
|
25
|
+
CONNECTION = 'connection'.freeze # Completely ignored. Why is CGI doing this?
|
|
26
|
+
CHARSET = 'charset'.freeze # this gets appended to Content-Type
|
|
27
|
+
COOKIE = 'cookie'.freeze # maps (Hash,Array,String) to "Set-Cookie" headers
|
|
28
|
+
STATUS = 'status'.freeze # stored as @status
|
|
29
|
+
Status = 'Status'.freeze # code + human-readable text, Rails sets this
|
|
30
|
+
|
|
31
|
+
# some of these are common strings, but this is the only module
|
|
32
|
+
# using them and the reason they're not in Unicorn::Const
|
|
33
|
+
SET_COOKIE = 'Set-Cookie'.freeze
|
|
34
|
+
CONTENT_TYPE = 'Content-Type'.freeze
|
|
35
|
+
CONTENT_LENGTH = 'Content-Length'.freeze # this is NOT Const::CONTENT_LENGTH
|
|
36
|
+
RACK_INPUT = 'rack.input'.freeze
|
|
37
|
+
RACK_ERRORS = 'rack.errors'.freeze
|
|
38
|
+
|
|
39
|
+
# this maps CGI header names to HTTP header names
|
|
40
|
+
HEADER_MAP = {
|
|
41
|
+
'status' => Status,
|
|
42
|
+
'type' => CONTENT_TYPE,
|
|
43
|
+
'server' => 'Server'.freeze,
|
|
44
|
+
'language' => 'Content-Language'.freeze,
|
|
45
|
+
'expires' => 'Expires'.freeze,
|
|
46
|
+
'length' => CONTENT_LENGTH,
|
|
47
|
+
}.freeze
|
|
48
|
+
|
|
49
|
+
# Takes an a Rackable environment, plus any additional CGI.new
|
|
50
|
+
# arguments These are used internally to create a wrapper around the
|
|
51
|
+
# real CGI while maintaining Rack/Unicorn's view of the world. This
|
|
52
|
+
# this will NOT deal well with large responses that take up a lot of
|
|
53
|
+
# memory, but neither does the CGI nor the original CGIWrapper from
|
|
54
|
+
# Mongrel...
|
|
55
|
+
def initialize(rack_env, *args)
|
|
56
|
+
@env_table = rack_env
|
|
57
|
+
@status = nil
|
|
58
|
+
@head = {}
|
|
59
|
+
@headv = Hash.new { |hash,key| hash[key] = [] }
|
|
60
|
+
@body = StringIO.new
|
|
61
|
+
super(*args)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# finalizes the response in a way Rack applications would expect
|
|
65
|
+
def rack_response
|
|
66
|
+
# @head[CONTENT_LENGTH] ||= @body.size
|
|
67
|
+
@headv[SET_COOKIE] += @output_cookies if @output_cookies
|
|
68
|
+
@headv.each_pair do |key,value|
|
|
69
|
+
@head[key] ||= value.join("\n") unless value.empty?
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Capitalized "Status:", with human-readable status code (e.g. "200 OK")
|
|
73
|
+
parseable_status = @head.delete(Status)
|
|
74
|
+
unless @status
|
|
75
|
+
@status ||= parseable_status.split(/ /)[0].to_i rescue 404
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
[ @status, @head, [ @body.string ] ]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# The header is typically called to send back the header. In our case we
|
|
82
|
+
# collect it into a hash for later usage. This can be called multiple
|
|
83
|
+
# times to set different cookies.
|
|
84
|
+
def header(options = "text/html")
|
|
85
|
+
# if they pass in a string then just write the Content-Type
|
|
86
|
+
if String === options
|
|
87
|
+
@head[CONTENT_TYPE] ||= options
|
|
88
|
+
else
|
|
89
|
+
HEADER_MAP.each_pair do |from, to|
|
|
90
|
+
from = options.delete(from) or next
|
|
91
|
+
@head[to] = from.to_s
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
@head[CONTENT_TYPE] ||= "text/html"
|
|
95
|
+
if charset = options.delete(CHARSET)
|
|
96
|
+
@head[CONTENT_TYPE] << "; charset=#{charset}"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# lots of ways to set cookies
|
|
100
|
+
if cookie = options.delete(COOKIE)
|
|
101
|
+
set_cookies = @headv[SET_COOKIE]
|
|
102
|
+
case cookie
|
|
103
|
+
when Array
|
|
104
|
+
cookie.each { |c| set_cookies << c.to_s }
|
|
105
|
+
when Hash
|
|
106
|
+
cookie.each_value { |c| set_cookies << c.to_s }
|
|
107
|
+
else
|
|
108
|
+
set_cookies << cookie.to_s
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
@status ||= options.delete(STATUS) # all lower-case
|
|
112
|
+
|
|
113
|
+
# drop the keys we don't want anymore
|
|
114
|
+
options.delete(NPH)
|
|
115
|
+
options.delete(CONNECTION)
|
|
116
|
+
|
|
117
|
+
# finally, set the rest of the headers as-is, allowing duplicates
|
|
118
|
+
options.each_pair { |k,v| @headv[k] << v }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# doing this fakes out the cgi library to think the headers are empty
|
|
122
|
+
# we then do the real headers in the out function call later
|
|
123
|
+
""
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# The dumb thing is people can call header or this or both and in
|
|
127
|
+
# any order. So, we just reuse header and then finalize the
|
|
128
|
+
# HttpResponse the right way. This will have no effect if called
|
|
129
|
+
# the second time if the first "outputted" anything.
|
|
130
|
+
def out(options = "text/html")
|
|
131
|
+
header(options)
|
|
132
|
+
@body.size == 0 or return
|
|
133
|
+
@body << yield if block_given?
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Used to wrap the normal stdinput variable used inside CGI.
|
|
137
|
+
def stdinput
|
|
138
|
+
@env_table[RACK_INPUT]
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# The stdoutput should be completely bypassed but we'll drop a
|
|
142
|
+
# warning just in case
|
|
143
|
+
def stdoutput
|
|
144
|
+
err = @env_table[RACK_ERRORS]
|
|
145
|
+
err.puts "WARNING: Your program is doing something not expected."
|
|
146
|
+
err.puts "Please tell Eric that stdoutput was used and what software " \
|
|
147
|
+
"you are running. Thanks."
|
|
148
|
+
@body
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
end
|