unicorn 0.2.3 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (95) hide show
  1. data/.document +1 -1
  2. data/.gitignore +1 -0
  3. data/CHANGELOG +1 -0
  4. data/DESIGN +4 -0
  5. data/GNUmakefile +30 -6
  6. data/Manifest +62 -3
  7. data/README +52 -42
  8. data/SIGNALS +17 -17
  9. data/TODO +27 -5
  10. data/bin/unicorn +15 -13
  11. data/bin/unicorn_rails +59 -22
  12. data/ext/unicorn/http11/http11.c +25 -104
  13. data/ext/unicorn/http11/http11_parser.c +24 -23
  14. data/ext/unicorn/http11/http11_parser.h +1 -3
  15. data/ext/unicorn/http11/http11_parser.rl +2 -1
  16. data/lib/unicorn.rb +58 -44
  17. data/lib/unicorn/app/old_rails.rb +23 -0
  18. data/lib/unicorn/app/old_rails/static.rb +58 -0
  19. data/lib/unicorn/cgi_wrapper.rb +151 -0
  20. data/lib/unicorn/configurator.rb +71 -31
  21. data/lib/unicorn/const.rb +9 -34
  22. data/lib/unicorn/http_request.rb +63 -66
  23. data/lib/unicorn/http_response.rb +6 -1
  24. data/lib/unicorn/socket.rb +15 -2
  25. data/test/benchmark/README +55 -0
  26. data/test/benchmark/big_request.rb +35 -0
  27. data/test/benchmark/dd.ru +18 -0
  28. data/test/benchmark/request.rb +47 -0
  29. data/test/benchmark/response.rb +29 -0
  30. data/test/exec/test_exec.rb +41 -157
  31. data/test/rails/app-1.2.3/.gitignore +2 -0
  32. data/test/rails/app-1.2.3/app/controllers/application.rb +4 -0
  33. data/test/rails/app-1.2.3/app/controllers/foo_controller.rb +34 -0
  34. data/test/rails/app-1.2.3/app/helpers/application_helper.rb +2 -0
  35. data/test/rails/app-1.2.3/config/boot.rb +9 -0
  36. data/test/rails/app-1.2.3/config/database.yml +12 -0
  37. data/test/rails/app-1.2.3/config/environment.rb +10 -0
  38. data/test/rails/app-1.2.3/config/environments/development.rb +7 -0
  39. data/test/rails/app-1.2.3/config/environments/production.rb +3 -0
  40. data/test/rails/app-1.2.3/config/routes.rb +4 -0
  41. data/test/rails/app-1.2.3/db/.gitignore +0 -0
  42. data/test/rails/app-1.2.3/public/404.html +1 -0
  43. data/test/rails/app-1.2.3/public/500.html +1 -0
  44. data/test/rails/app-2.0.2/.gitignore +2 -0
  45. data/test/rails/app-2.0.2/app/controllers/application.rb +2 -0
  46. data/test/rails/app-2.0.2/app/controllers/foo_controller.rb +34 -0
  47. data/test/rails/app-2.0.2/app/helpers/application_helper.rb +2 -0
  48. data/test/rails/app-2.0.2/config/boot.rb +9 -0
  49. data/test/rails/app-2.0.2/config/database.yml +12 -0
  50. data/test/rails/app-2.0.2/config/environment.rb +14 -0
  51. data/test/rails/app-2.0.2/config/environments/development.rb +6 -0
  52. data/test/rails/app-2.0.2/config/environments/production.rb +3 -0
  53. data/test/rails/app-2.0.2/config/routes.rb +4 -0
  54. data/test/rails/app-2.0.2/db/.gitignore +0 -0
  55. data/test/rails/app-2.0.2/public/404.html +1 -0
  56. data/test/rails/app-2.0.2/public/500.html +1 -0
  57. data/test/rails/app-2.2.2/.gitignore +2 -0
  58. data/test/rails/app-2.2.2/app/controllers/application.rb +2 -0
  59. data/test/rails/app-2.2.2/app/controllers/foo_controller.rb +34 -0
  60. data/test/rails/app-2.2.2/app/helpers/application_helper.rb +2 -0
  61. data/test/rails/app-2.2.2/config/boot.rb +109 -0
  62. data/test/rails/app-2.2.2/config/database.yml +12 -0
  63. data/test/rails/app-2.2.2/config/environment.rb +14 -0
  64. data/test/rails/app-2.2.2/config/environments/development.rb +5 -0
  65. data/test/rails/app-2.2.2/config/environments/production.rb +3 -0
  66. data/test/rails/app-2.2.2/config/routes.rb +4 -0
  67. data/test/rails/app-2.2.2/db/.gitignore +0 -0
  68. data/test/rails/app-2.2.2/public/404.html +1 -0
  69. data/test/rails/app-2.2.2/public/500.html +1 -0
  70. data/test/rails/app-2.3.2.1/.gitignore +2 -0
  71. data/test/rails/app-2.3.2.1/app/controllers/application_controller.rb +3 -0
  72. data/test/rails/app-2.3.2.1/app/controllers/foo_controller.rb +34 -0
  73. data/test/rails/app-2.3.2.1/app/helpers/application_helper.rb +2 -0
  74. data/test/rails/app-2.3.2.1/config/boot.rb +107 -0
  75. data/test/rails/app-2.3.2.1/config/database.yml +12 -0
  76. data/test/rails/app-2.3.2.1/config/environment.rb +14 -0
  77. data/test/rails/app-2.3.2.1/config/environments/development.rb +5 -0
  78. data/test/rails/app-2.3.2.1/config/environments/production.rb +4 -0
  79. data/test/rails/app-2.3.2.1/config/routes.rb +4 -0
  80. data/test/rails/app-2.3.2.1/db/.gitignore +0 -0
  81. data/test/rails/app-2.3.2.1/public/404.html +1 -0
  82. data/test/rails/app-2.3.2.1/public/500.html +1 -0
  83. data/test/rails/test_rails.rb +243 -0
  84. data/test/test_helper.rb +149 -2
  85. data/test/unit/test_configurator.rb +46 -0
  86. data/test/unit/test_http_parser.rb +77 -36
  87. data/test/unit/test_request.rb +2 -0
  88. data/test/unit/test_response.rb +20 -4
  89. data/test/unit/test_server.rb +30 -1
  90. data/test/unit/test_socket_helper.rb +159 -0
  91. data/unicorn.gemspec +5 -5
  92. metadata +68 -5
  93. data/test/benchmark/previous.rb +0 -11
  94. data/test/benchmark/simple.rb +0 -11
  95. 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, size_t off);
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, size_t off) {
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
 
@@ -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 && @pid != path
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, @backlog)
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('CHLD') { |sig_nr| awaken_master }
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 'QUIT' # graceful shutdown
179
+ when :QUIT # graceful shutdown
177
180
  break
178
- when 'TERM', 'INT' # immediate shutdown
181
+ when :TERM, :INT # immediate shutdown
179
182
  stop(false)
180
183
  break
181
- when 'USR1' # rotate logs
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('USR1')
186
- when 'USR2' # exec binary, stay alive in case something went wrong
188
+ kill_each_worker(:USR1)
189
+ when :USR2 # exec binary, stay alive in case something went wrong
187
190
  reexec
188
- when 'WINCH'
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('QUIT')
195
+ kill_each_worker(:QUIT)
193
196
  else
194
197
  logger.info "SIGWINCH ignored because we're not daemonized"
195
198
  end
196
- when 'HUP'
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 ? 'QUIT' : 'TERM')
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('KILL')
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.sysread(Const::CHUNK_SIZE) }
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.syswrite('.') # wakeup master process from IO.select
270
- rescue Errno::EAGAIN # pipe is full, master should wake up anyways
271
- rescue Errno::EINTR
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('KILL', pid) # take no prisoners for @timeout violations
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 << 'QUIT' # forcibly emulate SIGQUIT
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
- env = @request.read(client) or return
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.closed? or client.close rescue nil
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('CHLD', 'DEFAULT')
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
- %w(TERM INT).each { |sig| trap(sig) { exit(0) } } # instant shutdown
439
- trap('QUIT') do
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 = client.sync = true
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
- if client && !client.closed?
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, nil, @timeout/2.0) or next
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('QUIT')
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