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.
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