unicorn 0.2.1 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
data/.document CHANGED
@@ -5,7 +5,8 @@ CONTRIBUTORS
5
5
  LICENSE
6
6
  SIGNALS
7
7
  TODO
8
- bin
8
+ bin/unicorn
9
+ bin/unicorn_rails
9
10
  lib
10
11
  ext/**/*.c
11
12
  ext/**/*.rl
data/CHANGELOG CHANGED
@@ -1,25 +1,4 @@
1
+ v0.2.2 - small bug fixes, fix Rack multi-value headers (Set-Cookie:)
1
2
  v0.2.1 - Fix broken Manifest that cause unicorn_rails to not be bundled
2
-
3
3
  v0.2.0 - unicorn_rails launcher script.
4
-
5
4
  v0.1.0 - Unicorn - UNIX-only fork of Mongrel free of threading
6
-
7
- -- old Mongrel changelog --
8
-
9
- v2.0. (WIP) Rack support.
10
-
11
- v1.1.4. Fix camping handler. Correct treatment of @throttle parameter.
12
-
13
- v1.1.3. Fix security flaw of DirHandler; reported on mailing list.
14
-
15
- v1.1.2. Fix worker termination bug; fix JRuby 1.0.3 load order issue; fix require issue on systems without Rubygems.
16
-
17
- v1.1.1. Fix mongrel_rails restart bug; fix bug with Rack status codes.
18
-
19
- v1.1. Pure Ruby URIClassifier. More modular architecture. JRuby support. Move C URIClassifier into mongrel_experimental project.
20
-
21
- v1.0.4. Backport fixes for versioning inconsistency, mongrel_rails bug, and DirHandler bug.
22
-
23
- v1.0.3. Fix user-switching bug; make people upgrade to the latest from the RC.
24
-
25
- v1.0.2. Signed gem; many minor bugfixes and patches.
data/Manifest CHANGED
@@ -20,10 +20,12 @@ ext/unicorn/http11/http11_parser.h
20
20
  ext/unicorn/http11/http11_parser.rl
21
21
  ext/unicorn/http11/http11_parser_common.rl
22
22
  lib/unicorn.rb
23
+ lib/unicorn/app/exec_cgi.rb
23
24
  lib/unicorn/configurator.rb
24
25
  lib/unicorn/const.rb
25
26
  lib/unicorn/http_request.rb
26
27
  lib/unicorn/http_response.rb
28
+ lib/unicorn/launcher.rb
27
29
  lib/unicorn/socket.rb
28
30
  lib/unicorn/util.rb
29
31
  setup.rb
@@ -37,6 +39,7 @@ test/test_helper.rb
37
39
  test/tools/trickletest.rb
38
40
  test/unit/test_configurator.rb
39
41
  test/unit/test_http_parser.rb
42
+ test/unit/test_request.rb
40
43
  test/unit/test_response.rb
41
44
  test/unit/test_server.rb
42
45
  test/unit/test_upload.rb
data/README CHANGED
@@ -49,7 +49,7 @@ least a friend who can build it for you.
49
49
  You may download the tarball from the Mongrel project page on Rubyforge
50
50
  and run setup.rb after unpacking it:
51
51
 
52
- http://rubyforge.org/frs/?group_id=1306
52
+ http://rubyforge.org/frs/?group_id=1306
53
53
 
54
54
  You may also install it via Rubygems on Rubyforge:
55
55
 
@@ -67,7 +67,7 @@ If you have web browser software for the World Wide Web
67
67
  (on the Information Superhighway), you may browse the code from
68
68
  your web browser and download the latest snapshot tarballs here:
69
69
 
70
- * http://git.bogomips.org/cgit/unicorn.git
70
+ * http://git.bogomips.org/cgit/unicorn.git (this server runs Unicorn!)
71
71
  * http://repo.or.cz/w/unicorn.git (gitweb mirror)
72
72
 
73
73
  == Usage
@@ -103,12 +103,16 @@ functionality of the `unicorn' launcher.
103
103
 
104
104
  == Disclaimer
105
105
 
106
- There are no known production instances of unicorn deployed
107
- anywhere in the world. The original author of unicorn only has
108
- one, internal, low-traffic Sinatra application deployed with it.
109
- Maybe you'll be the first guinea pig to test it in production.
110
- Of course there is NO WARRANTY whatsoever if anything goes wrong,
111
- but let us know and we'll try our best to fix it.
106
+ There are only a few instances of Unicorn deployed anywhere in the
107
+ world. The only public site known to run Unicorn at this time is
108
+ http://git.bogomips.org/cgit which runs Unicorn::App::ExecCgi to
109
+ fork()+exec() cgit.
110
+
111
+ Be one of the first brave guinea pigs to run it on your production site!
112
+ Of course there is NO WARRANTY whatsoever if anything goes wrong, but
113
+ let us know and we'll try our best to fix it. Unicorn is still in the
114
+ early stages and testing + feedback would be *greatly* appreciated;
115
+ maybe you'll get Rainbows as a reward!
112
116
 
113
117
  == Known Issues
114
118
 
data/SIGNALS CHANGED
@@ -22,6 +22,9 @@ processes are documented here as well.
22
22
  should be sent to the original process once the child is verified to
23
23
  be up and running.
24
24
 
25
+ * WINCH - gracefully stops workers but keep the master running.
26
+ This will only work for daemonized processes.
27
+
25
28
  === Worker Processes
26
29
 
27
30
  Sending signals directly to the worker processes should not normally be
@@ -34,3 +37,51 @@ automatically respawned.
34
37
 
35
38
  * USR1 - reopen all logs owned by the worker process
36
39
  See Unicorn::Util.reopen_logs for what is considered a log.
40
+
41
+ === Procedure to replace a running unicorn executable
42
+
43
+ You may replace a running instance of unicorn with a new one without
44
+ losing any incoming connections. Doing so will reload all of your
45
+ application code, Unicorn config, Ruby executable, and all libraries.
46
+ The only things that will not change (due to OS limitations) are:
47
+
48
+ 1. The listener backlog size of already-bound sockets
49
+
50
+ 2. The path to the unicorn executable script. If you want to change to
51
+ a different installation of Ruby, you can modify the shebang
52
+ line to point to your alternative interpreter.
53
+
54
+ The procedure is exactly like that of nginx:
55
+
56
+ 1. Send USR2 to the master process
57
+
58
+ 2. Check your process manager or pid files to see if a new master spawned
59
+ successfully. If you're using a pid file, the old process will have
60
+ ".oldbin" appended to its path. You should have two master instances
61
+ of unicorn running now, both of which will have workers servicing
62
+ requests. Your process tree should look something like this:
63
+
64
+ unicorn master (old)
65
+ \_ unicorn worker[0]
66
+ \_ unicorn worker[1]
67
+ \_ unicorn worker[2]
68
+ \_ unicorn worker[3]
69
+ \_ unicorn master
70
+ \_ unicorn worker[0]
71
+ \_ unicorn worker[1]
72
+ \_ unicorn worker[2]
73
+ \_ unicorn worker[3]
74
+
75
+ 4. You can now send WINCH to the old master process so only the new workers
76
+ serve requests. If your unicorn process is bound to an interactive
77
+ terminal, you can skip this step. Step 5 will be more difficult but
78
+ you can also skip it if your process is not daemonized.
79
+
80
+ 5. You should now ensure that everything is running correctly with the
81
+ new workers as the old workers die off.
82
+
83
+ 6a. If everything seems ok, then send QUIT to the old master. You're done!
84
+
85
+ 6b. If something is broken, then send HUP to the old master to reload
86
+ the config and restart its workers. Then send QUIT to the new master
87
+ process.
data/bin/unicorn CHANGED
@@ -1,6 +1,5 @@
1
1
  #!/home/ew/bin/ruby
2
- $stdin.sync = $stdout.sync = $stderr.sync = true
3
- require 'unicorn' # require this first to populate Unicorn::DEFAULT_START_CTX
2
+ require 'unicorn/launcher'
4
3
  require 'optparse'
5
4
 
6
5
  env = "development"
@@ -163,27 +162,5 @@ if $DEBUG
163
162
  })
164
163
  end
165
164
 
166
- # only daemonize if we're not inheriting file descriptors from our parent
167
- if daemonize
168
-
169
- $stdin.reopen("/dev/null")
170
- unless ENV['UNICORN_FD']
171
- exit if fork
172
- Process.setsid
173
- exit if fork
174
- end
175
-
176
- # We don't do a lot of standard daemonization stuff:
177
- # * $stderr/$stderr can/will be redirected separately
178
- # * umask is whatever was set by the parent process at startup
179
- # and can be set in config.ru and config_file, so making it
180
- # 0000 and potentially exposing sensitive log data can be bad
181
- # policy.
182
- # * Don't bother to chdir here since Unicorn is designed to
183
- # run inside APP_ROOT. Unicorn will also re-chdir() to
184
- # the directory it was started in when being re-executed
185
- # to pickup code changes if the original deployment directory
186
- # is a symlink or otherwise got replaced.
187
- end
188
-
165
+ Unicorn::Launcher.daemonize! if daemonize
189
166
  Unicorn.run(app, options)
data/bin/unicorn_rails CHANGED
@@ -1,6 +1,5 @@
1
1
  #!/home/ew/bin/ruby
2
- $stdin.sync = $stdout.sync = $stderr.sync = true
3
- require 'unicorn' # require this first to populate Unicorn::DEFAULT_START_CTX
2
+ require 'unicorn/launcher'
4
3
  require 'optparse'
5
4
  require 'fileutils'
6
5
 
@@ -118,49 +117,12 @@ rails_loader = lambda do ||
118
117
  abort "#$0 must be run inside RAILS_ROOT (#{::RAILS_ROOT})"
119
118
  end
120
119
 
121
- if ENV['UNICORN_RAILS_USE_SYSTEM_RACK'].to_i == 0
122
- rails_ver = Rails::VERSION::STRING
123
-
124
- # maps Rails versions to the vendorized Rack version they bundle
125
- version_map = {
126
- '2.3.2' => '1.0',
127
- # 3.0.0 => false, # assuming 3.0.0 doesn't need vendorized Rack anymore
128
- }
129
- rack_ver = version_map[rails_ver] or
130
- warn "Possibly unsupported Rails version: v#{rails_ver}"
131
-
132
- rack_path = nil
133
- case rack_ver
134
- when String, NilClass
135
- version_map.values.find_all { |v| String === v }.sort.each do |v|
136
- $LOAD_PATH.grep(%r{/actionpack-[\d\.]+/lib/?\z}).each do |path|
137
- rack_path = File.join(path, "action_controller/vendor/rack-#{v}")
138
- File.directory?(rack_path) and break
139
- rack_path = nil
140
- end
141
- break if rack_path
142
- end
143
- rack_path or abort(
144
- "Unable to find Rails-vendorized Rack library.\n" \
145
- "Perhaps this script is no longer with your" \
146
- "Rails version (#{rails_ver}).\n")
147
- puts "vendorized Rack load path #{rack_path}"
148
- $LOAD_PATH.unshift(rack_path)
149
- when FalseClass
150
- # using non-vendorized rack library (most likely via gems)
151
- end
152
- end # Vendorized Rack LOAD_PATH finder
153
-
154
- # require Rack as late as possible in case $LOAD_PATH is modified
155
- # in config.ru or command-line
156
- require 'rack'
157
-
158
120
  # return the lambda
159
121
  config = ::ARGV[0] || (File.exist?('config.ru') ? 'config.ru' : nil)
160
122
  case config
161
123
  when nil
162
124
  lambda do ||
163
- require "#{RAILS_ROOT}/config/environment"
125
+ require 'config/environment'
164
126
  ActionController::Dispatcher.new
165
127
  end
166
128
  when /\.ru$/
@@ -208,31 +170,13 @@ if $DEBUG
208
170
  })
209
171
  end
210
172
 
211
- # only daemonize if we're not inheriting file descriptors from our parent
212
- if daemonize
213
- options[:pid] = rails_pid
214
- $stdin.reopen("/dev/null")
215
- unless ENV['UNICORN_FD']
216
- exit if fork
217
- Process.setsid
218
- exit if fork
219
- end
220
-
221
- # We don't do a lot of standard daemonization stuff:
222
- # * $stderr/$stderr can/will be redirected separately
223
- # * umask is whatever was set by the parent process at startup
224
- # and can be set in config.ru and config_file, so making it
225
- # 0000 and potentially exposing sensitive log data can be bad
226
- # policy.
227
- # * Don't bother to chdir here since Unicorn is designed to
228
- # run inside APP_ROOT. Unicorn will also re-chdir() to
229
- # the directory it was started in when being re-executed
230
- # to pickup code changes if the original deployment directory
231
- # is a symlink or otherwise got replaced.
232
- end
233
-
234
173
  # ensure Rails standard tmp paths exist
235
174
  %w(cache pids sessions sockets).each do |dir|
236
175
  FileUtils.mkdir_p("tmp/#{dir}")
237
176
  end
177
+
178
+ if daemonize
179
+ options[:pid] = rails_pid
180
+ Unicorn::Launcher.daemonize!
181
+ end
238
182
  Unicorn.run(app, options)
@@ -28,13 +28,9 @@ static VALUE global_fragment;
28
28
  static VALUE global_query_string;
29
29
  static VALUE global_http_version;
30
30
  static VALUE global_content_length;
31
- static VALUE global_http_content_length;
32
31
  static VALUE global_request_path;
33
32
  static VALUE global_content_type;
34
- static VALUE global_http_content_type;
35
33
  static VALUE global_http_body;
36
- static VALUE global_gateway_interface;
37
- static VALUE global_gateway_interface_value;
38
34
  static VALUE global_server_name;
39
35
  static VALUE global_server_port;
40
36
  static VALUE global_server_protocol;
@@ -129,6 +125,7 @@ static int common_field_cmp(const void *a, const void *b)
129
125
  }
130
126
  #endif /* HAVE_QSORT_BSEARCH */
131
127
 
128
+ /* this function is not performance-critical */
132
129
  static void init_common_fields(void)
133
130
  {
134
131
  int i;
@@ -137,8 +134,15 @@ static void init_common_fields(void)
137
134
  memcpy(tmp, HTTP_PREFIX, HTTP_PREFIX_LEN);
138
135
 
139
136
  for(i = 0; i < ARRAY_SIZE(common_http_fields); cf++, i++) {
140
- memcpy(tmp + HTTP_PREFIX_LEN, cf->name, cf->len + 1);
141
- cf->value = rb_obj_freeze(rb_str_new(tmp, HTTP_PREFIX_LEN + cf->len));
137
+ /* Rack doesn't like certain headers prefixed with "HTTP_" */
138
+ if (!strcmp("CONTENT_LENGTH", cf->name) ||
139
+ !strcmp("CONTENT_TYPE", cf->name)) {
140
+ cf->value = rb_str_new(cf->name, cf->len);
141
+ } else {
142
+ memcpy(tmp + HTTP_PREFIX_LEN, cf->name, cf->len + 1);
143
+ cf->value = rb_str_new(tmp, HTTP_PREFIX_LEN + cf->len);
144
+ }
145
+ cf->value = rb_obj_freeze(cf->value);
142
146
  rb_global_variable(&cf->value);
143
147
  }
144
148
 
@@ -275,21 +279,8 @@ static void header_done(void *data, const char *at, size_t length)
275
279
  {
276
280
  VALUE req = (VALUE)data;
277
281
  VALUE temp = Qnil;
278
- VALUE ctype = Qnil;
279
- VALUE clen = Qnil;
280
282
  char *colon = NULL;
281
283
 
282
- clen = rb_hash_aref(req, global_http_content_length);
283
- if(clen != Qnil) {
284
- rb_hash_aset(req, global_content_length, clen);
285
- }
286
-
287
- ctype = rb_hash_aref(req, global_http_content_type);
288
- if(ctype != Qnil) {
289
- rb_hash_aset(req, global_content_type, ctype);
290
- }
291
-
292
- rb_hash_aset(req, global_gateway_interface, global_gateway_interface_value);
293
284
  if((temp = rb_hash_aref(req, global_http_host)) != Qnil) {
294
285
  colon = memchr(RSTRING_PTR(temp), ':', RSTRING_LEN(temp));
295
286
  if(colon != NULL) {
@@ -497,12 +488,8 @@ void Init_http11()
497
488
  DEF_GLOBAL(http_version, "HTTP_VERSION");
498
489
  DEF_GLOBAL(request_path, "REQUEST_PATH");
499
490
  DEF_GLOBAL(content_length, "CONTENT_LENGTH");
500
- DEF_GLOBAL(http_content_length, "HTTP_CONTENT_LENGTH");
501
491
  DEF_GLOBAL(http_body, "HTTP_BODY");
502
492
  DEF_GLOBAL(content_type, "CONTENT_TYPE");
503
- DEF_GLOBAL(http_content_type, "HTTP_CONTENT_TYPE");
504
- DEF_GLOBAL(gateway_interface, "GATEWAY_INTERFACE");
505
- DEF_GLOBAL(gateway_interface_value, "CGI/1.2");
506
493
  DEF_GLOBAL(server_name, "SERVER_NAME");
507
494
  DEF_GLOBAL(server_port, "SERVER_PORT");
508
495
  DEF_GLOBAL(server_protocol, "SERVER_PROTOCOL");
data/lib/unicorn.rb CHANGED
@@ -23,7 +23,6 @@ module Unicorn
23
23
  # forked worker children.
24
24
  class HttpServer
25
25
  attr_reader :logger
26
- include Process
27
26
  include ::Unicorn::SocketHelper
28
27
 
29
28
  DEFAULT_START_CTX = {
@@ -160,7 +159,8 @@ module Unicorn
160
159
  # are trapped. See trap_deferred
161
160
  @rd_sig, @wr_sig = IO.pipe unless (@rd_sig && @wr_sig)
162
161
  @rd_sig.nonblock = @wr_sig.nonblock = true
163
- ready = mode = nil
162
+ mode = nil
163
+ respawn = true
164
164
 
165
165
  QUEUE_SIGS.each { |sig| trap_deferred(sig) }
166
166
  trap('CHLD') { |sig_nr| awaken_master }
@@ -172,18 +172,30 @@ module Unicorn
172
172
  case (mode = @sig_queue.shift)
173
173
  when nil
174
174
  murder_lazy_workers
175
- spawn_missing_workers
175
+ spawn_missing_workers if respawn
176
+ master_sleep
176
177
  when 'QUIT' # graceful shutdown
177
178
  break
178
179
  when 'TERM', 'INT' # immediate shutdown
179
180
  stop(false)
180
181
  break
181
182
  when 'USR1' # rotate logs
182
- kill_each_worker('USR1')
183
+ logger.info "master rotating logs..."
183
184
  Unicorn::Util.reopen_logs
185
+ logger.info "master done rotating logs"
186
+ kill_each_worker('USR1')
184
187
  when 'USR2' # exec binary, stay alive in case something went wrong
185
188
  reexec
189
+ when 'WINCH'
190
+ if Process.ppid == 1 || Process.getpgrp != $$
191
+ respawn = false
192
+ logger.info "gracefully stopping all workers"
193
+ kill_each_worker('QUIT')
194
+ else
195
+ logger.info "SIGWINCH ignored because we're not daemonized"
196
+ end
186
197
  when 'HUP'
198
+ respawn = true
187
199
  if @config.config_file
188
200
  load_config!
189
201
  redo # immediate reaping since we may have QUIT workers
@@ -195,18 +207,6 @@ module Unicorn
195
207
  else
196
208
  logger.error "master process in unknown mode: #{mode}"
197
209
  end
198
- reap_all_workers
199
-
200
- ready = begin
201
- IO.select([@rd_sig], nil, nil, 1) or next
202
- rescue Errno::EINTR # next
203
- end
204
- ready[0] && ready[0][0] or next
205
- begin
206
- @rd_sig.sysread(1)
207
- rescue Errno::EAGAIN, Errno::EINTR
208
- # spurious wakeup? ignore it
209
- end
210
210
  end
211
211
  rescue Errno::EINTR
212
212
  retry
@@ -239,7 +239,8 @@ module Unicorn
239
239
  private
240
240
 
241
241
  # list of signals we care about and trap in master.
242
- QUEUE_SIGS = %w(QUIT INT TERM USR1 USR2 HUP).map { |x| x.freeze }.freeze
242
+ QUEUE_SIGS =
243
+ %w(WINCH QUIT INT TERM USR1 USR2 HUP).map { |x| x.freeze }.freeze
243
244
 
244
245
  # defer a signal for later processing in #join (master process)
245
246
  def trap_deferred(signal)
@@ -253,6 +254,17 @@ module Unicorn
253
254
  end
254
255
  end
255
256
 
257
+ # wait for a signal hander to wake us up and then consume the pipe
258
+ # Wake up every second anyways to run murder_lazy_workers
259
+ def master_sleep
260
+ begin
261
+ ready = IO.select([@rd_sig], nil, nil, 1)
262
+ ready && ready[0] && ready[0][0] or return
263
+ loop { @rd_sig.sysread(Const::CHUNK_SIZE) }
264
+ rescue Errno::EAGAIN, Errno::EINTR
265
+ end
266
+ end
267
+
256
268
  def awaken_master
257
269
  begin
258
270
  @wr_sig.syswrite('.') # wakeup master process from IO.select
@@ -266,17 +278,18 @@ module Unicorn
266
278
  def reap_all_workers
267
279
  begin
268
280
  loop do
269
- pid = waitpid(-1, WNOHANG) or break
281
+ pid, status = Process.waitpid2(-1, Process::WNOHANG)
282
+ pid or break
270
283
  if @reexec_pid == pid
271
- logger.error "reaped exec()-ed PID:#{pid} status=#{$?.exitstatus}"
284
+ logger.error "reaped #{status.inspect} exec()-ed"
272
285
  @reexec_pid = 0
273
286
  self.pid = @pid.chomp('.oldbin') if @pid
287
+ $0 = "unicorn master"
274
288
  else
275
289
  worker = @workers.delete(pid)
276
290
  worker.tempfile.close rescue nil
277
- logger.info "reaped PID:#{pid} " \
278
- "worker=#{worker.nr rescue 'unknown'} " \
279
- "status=#{$?.exitstatus}"
291
+ logger.info "reaped #{status.inspect} " \
292
+ "worker=#{worker.nr rescue 'unknown'}"
280
293
  end
281
294
  end
282
295
  rescue Errno::ECHILD
@@ -324,6 +337,7 @@ module Unicorn
324
337
  @before_exec.call(self) if @before_exec
325
338
  exec(*cmd)
326
339
  end
340
+ $0 = "unicorn master (old)"
327
341
  end
328
342
 
329
343
  # forcibly terminate all workers that haven't checked in in @timeout
@@ -428,7 +442,7 @@ module Unicorn
428
442
  @listeners.each { |sock| sock.close rescue nil } # break IO.select
429
443
  end
430
444
 
431
- while alive && @master_pid == ppid
445
+ while alive && @master_pid == Process.ppid
432
446
  # we're a goner in @timeout seconds anyways if tempfile.chmod
433
447
  # breaks, so don't trap the exception. Using fchmod() since
434
448
  # futimes() is not available in base Ruby and I very strongly
@@ -494,7 +508,7 @@ module Unicorn
494
508
  # is no longer running.
495
509
  def kill_worker(signal, pid)
496
510
  begin
497
- kill(signal, pid)
511
+ Process.kill(signal, pid)
498
512
  rescue Errno::ESRCH
499
513
  worker = @workers.delete(pid) and worker.tempfile.close rescue nil
500
514
  end
@@ -516,7 +530,7 @@ module Unicorn
516
530
  def valid_pid?(path)
517
531
  if File.exist?(path) && (pid = File.read(path).to_i) > 1
518
532
  begin
519
- kill(0, pid)
533
+ Process.kill(0, pid)
520
534
  return pid
521
535
  rescue Errno::ESRCH
522
536
  end
@@ -0,0 +1,150 @@
1
+ require 'unicorn'
2
+ require 'rack'
3
+
4
+ module Unicorn::App
5
+
6
+ # This class is highly experimental (even more so than the rest of Unicorn)
7
+ # and has never run anything other than cgit.
8
+ class ExecCgi
9
+
10
+ CHUNK_SIZE = 16384
11
+ PASS_VARS = %w(
12
+ CONTENT_LENGTH
13
+ CONTENT_TYPE
14
+ GATEWAY_INTERFACE
15
+ AUTH_TYPE
16
+ PATH_INFO
17
+ PATH_TRANSLATED
18
+ QUERY_STRING
19
+ REMOTE_ADDR
20
+ REMOTE_HOST
21
+ REMOTE_IDENT
22
+ REMOTE_USER
23
+ REQUEST_METHOD
24
+ SERVER_NAME
25
+ SERVER_PORT
26
+ SERVER_PROTOCOL
27
+ SERVER_SOFTWARE
28
+ ).map { |x| x.freeze }.freeze # frozen strings are faster for Hash lookups
29
+
30
+ # Intializes the app, example of usage in a config.ru
31
+ # map "/cgit" do
32
+ # run Unicorn::App::ExecCgi.new("/path/to/cgit.cgi")
33
+ # end
34
+ def initialize(*args)
35
+ @args = args.dup
36
+ first = @args[0] or
37
+ raise ArgumentError, "need path to executable"
38
+ first[0..0] == "/" or @args[0] = ::File.expand_path(first)
39
+ File.executable?(@args[0]) or
40
+ raise ArgumentError, "#{@args[0]} is not executable"
41
+ end
42
+
43
+ # Calls the app
44
+ def call(env)
45
+ out, err = Tempfile.new(''), Tempfile.new('')
46
+ out.unlink
47
+ err.unlink
48
+ inp = force_file_input(env)
49
+ inp.sync = out.sync = err.sync = true
50
+ pid = fork { run_child(inp, out, err, env) }
51
+ inp.close
52
+ pid, status = Process.waitpid2(pid)
53
+ write_errors(env, err, status) if err.stat.size > 0
54
+ err.close
55
+
56
+ return parse_output!(out) if status.success?
57
+ out.close
58
+ [ 500, { 'Content-Length' => '0', 'Content-Type' => 'text/plain' }, [] ]
59
+ end
60
+
61
+ private
62
+
63
+ def run_child(inp, out, err, env)
64
+ PASS_VARS.each do |key|
65
+ val = env[key] or next
66
+ ENV[key] = val
67
+ end
68
+ ENV['SCRIPT_NAME'] = @args[0]
69
+ ENV['GATEWAY_INTERFACE'] = 'CGI/1.1'
70
+ env.keys.grep(/^HTTP_/) { |key| ENV[key] = env[key] }
71
+
72
+ IO.new(0).reopen(inp)
73
+ IO.new(1).reopen(out)
74
+ IO.new(2).reopen(err)
75
+ exec(*@args)
76
+ end
77
+
78
+ # Extracts headers from CGI out, will change the offset of out.
79
+ # This returns a standard Rack-compatible return value:
80
+ # [ 200, HeadersHash, body ]
81
+ def parse_output!(out)
82
+ size = out.stat.size
83
+ out.sysseek(0)
84
+ head = out.sysread(CHUNK_SIZE)
85
+ offset = 2
86
+ head, body = head.split(/\n\n/, 2)
87
+ if body.nil?
88
+ head, body = head.split(/\r\n\r\n/, 2)
89
+ offset = 4
90
+ end
91
+ offset += head.length
92
+ out.instance_variable_set('@unicorn_app_exec_cgi_offset', offset)
93
+ size -= offset
94
+
95
+ # Allows +out+ to be used as a Rack body.
96
+ def out.each
97
+ sysseek(@unicorn_app_exec_cgi_offset)
98
+ begin
99
+ loop { yield(sysread(CHUNK_SIZE)) }
100
+ rescue EOFError
101
+ end
102
+ end
103
+
104
+ prev = nil
105
+ headers = Rack::Utils::HeaderHash.new
106
+ head.split(/\r?\n/).each do |line|
107
+ case line
108
+ when /^([A-Za-z0-9-]+):\s*(.*)$/ then headers[prev = $1] = $2
109
+ when /^[ \t]/ then headers[prev] << "\n#{line}" if prev
110
+ end
111
+ end
112
+ headers['Content-Length'] = size.to_s
113
+ [ 200, headers, out ]
114
+ end
115
+
116
+ # ensures rack.input is a file handle that we can redirect stdin to
117
+ def force_file_input(env)
118
+ inp = env['rack.input']
119
+ if inp.respond_to?(:fileno) && Integer === inp.fileno
120
+ inp
121
+ elsif inp.size == 0 # inp could be a StringIO or StringIO-like object
122
+ ::File.open('/dev/null')
123
+ else
124
+ tmp = Tempfile.new('')
125
+ tmp.unlink
126
+ tmp.binmode
127
+
128
+ # Rack::Lint::InputWrapper doesn't allow sysread :(
129
+ while buf = inp.read(CHUNK_SIZE)
130
+ tmp.syswrite(buf)
131
+ end
132
+ tmp.sysseek(0)
133
+ tmp
134
+ end
135
+ end
136
+
137
+ # rack.errors this may not be an IO object, so we couldn't
138
+ # just redirect the CGI executable to that earlier.
139
+ def write_errors(env, err, status)
140
+ err.seek(0)
141
+ dst = env['rack.errors']
142
+ pid = status.pid
143
+ dst.write("#{pid}: #{@args.inspect} status=#{status} stderr:\n")
144
+ err.each_line { |line| dst.write("#{pid}: #{line}") }
145
+ dst.flush
146
+ end
147
+
148
+ end
149
+
150
+ end
@@ -173,13 +173,14 @@ module Unicorn
173
173
  # worker processes. For per-worker listeners, see the after_fork example
174
174
  def listeners(addresses)
175
175
  Array === addresses or addresses = Array(addresses)
176
+ addresses.map! { |addr| expand_addr(addr) }
176
177
  @set[:listeners] = addresses
177
178
  end
178
179
 
179
180
  # adds an +address+ to the existing listener set
180
181
  def listen(address)
181
182
  @set[:listeners] = [] unless Array === @set[:listeners]
182
- @set[:listeners] << address
183
+ @set[:listeners] << expand_addr(address)
183
184
  end
184
185
 
185
186
  # sets the +path+ for the PID file of the unicorn master process
@@ -253,5 +254,17 @@ module Unicorn
253
254
  @set[var] = my_proc
254
255
  end
255
256
 
257
+ # expands pathnames of sockets if relative to "~" or "~username"
258
+ # expands "*:port and ":port" to "0.0.0.0:port"
259
+ def expand_addr(address) #:nodoc
260
+ return address unless String === address
261
+ if address[0..0] == '~'
262
+ return File.expand_path(address)
263
+ elsif address =~ %r{\A\*?:(\d+)\z}
264
+ return "0.0.0.0:#$1"
265
+ end
266
+ address
267
+ end
268
+
256
269
  end
257
270
  end
data/lib/unicorn/const.rb CHANGED
@@ -68,7 +68,7 @@ module Unicorn
68
68
  REQUEST_URI='REQUEST_URI'.freeze
69
69
  REQUEST_PATH='REQUEST_PATH'.freeze
70
70
 
71
- UNICORN_VERSION="0.2.1".freeze
71
+ UNICORN_VERSION="0.2.2".freeze
72
72
 
73
73
  UNICORN_TMP_BASE="unicorn".freeze
74
74
 
@@ -130,8 +130,6 @@ module Unicorn
130
130
  raise "No REQUEST PATH" unless @params[Const::REQUEST_PATH]
131
131
 
132
132
  @params["QUERY_STRING"] ||= ''
133
- @params.delete "HTTP_CONTENT_TYPE"
134
- @params.delete "HTTP_CONTENT_LENGTH"
135
133
  @params.update({ "rack.version" => [0,1],
136
134
  "rack.input" => @body,
137
135
  "rack.errors" => $stderr,
@@ -155,7 +153,7 @@ module Unicorn
155
153
  end
156
154
  true # success!
157
155
  rescue Object => e
158
- logger.error "Error reading HTTP body: #{e.inspect}"
156
+ @logger.error "Error reading HTTP body: #{e.inspect}"
159
157
  socket.closed? or socket.close rescue nil
160
158
 
161
159
  # Any errors means we should delete the file, including if the file
@@ -21,35 +21,32 @@ module Unicorn
21
21
 
22
22
  class HttpResponse
23
23
 
24
- # headers we allow duplicates for
25
- ALLOWED_DUPLICATES = {
26
- 'Set-Cookie' => true,
27
- 'Set-Cookie2' => true,
28
- 'Warning' => true,
29
- 'WWW-Authenticate' => true,
30
- }.freeze
24
+ # Rack does not set/require a Date: header. We always override the
25
+ # Connection: and Date: headers no matter what (if anything) our
26
+ # Rack application sent us.
27
+ SKIP = { 'connection' => true, 'date' => true }.freeze
31
28
 
32
29
  # writes the rack_response to socket as an HTTP response
33
30
  def self.write(socket, rack_response)
34
31
  status, headers, body = rack_response
32
+ out = [ "Date: #{Time.now.httpdate}" ]
35
33
 
36
- # Rack does not set/require Date, but don't worry about Content-Length
37
- # since Rack applications that conform to Rack::Lint enforce that
38
- out = [ "#{Const::DATE}: #{Time.now.httpdate}" ]
39
- sent = { Const::CONNECTION => true, Const::DATE => true }
40
-
34
+ # Don't bother enforcing duplicate supression, it's a Hash most of
35
+ # the time anyways so just hope our app knows what it's doing
41
36
  headers.each do |key, value|
42
- if ! sent[key] || ALLOWED_DUPLICATES[key]
43
- sent[key] = true
44
- out << "#{key}: #{value}"
45
- end
37
+ next if SKIP.include?(key.downcase)
38
+ value.split(/\n/).each { |v| out << "#{key}: #{v}" }
46
39
  end
47
40
 
41
+ # Rack should enforce Content-Length or chunked transfer encoding,
42
+ # so don't worry or care about them.
48
43
  socket_write(socket,
49
44
  "HTTP/1.1 #{status} #{HTTP_STATUS_CODES[status]}\r\n" \
50
45
  "Connection: close\r\n" \
51
46
  "#{out.join("\r\n")}\r\n\r\n")
52
47
  body.each { |chunk| socket_write(socket, chunk) }
48
+ ensure
49
+ body.respond_to?(:close) and body.close rescue nil
53
50
  end
54
51
 
55
52
  private
@@ -0,0 +1,33 @@
1
+ $stdin.sync = $stdout.sync = $stderr.sync = true
2
+ require 'unicorn'
3
+
4
+ class Unicorn::Launcher
5
+
6
+ # We don't do a lot of standard daemonization stuff:
7
+ # * umask is whatever was set by the parent process at startup
8
+ # and can be set in config.ru and config_file, so making it
9
+ # 0000 and potentially exposing sensitive log data can be bad
10
+ # policy.
11
+ # * don't bother to chdir("/") here since unicorn is designed to
12
+ # run inside APP_ROOT. Unicorn will also re-chdir() to
13
+ # the directory it was started in when being re-executed
14
+ # to pickup code changes if the original deployment directory
15
+ # is a symlink or otherwise got replaced.
16
+ def self.daemonize!
17
+ $stdin.reopen("/dev/null")
18
+
19
+ # We only start a new process group if we're not being reexecuted
20
+ # and inheriting file descriptors from our parent
21
+ unless ENV['UNICORN_FD']
22
+ exit if fork
23
+ Process.setsid
24
+ exit if fork
25
+
26
+ # $stderr/$stderr can/will be redirected separately in the Unicorn config
27
+ $stdout.reopen("/dev/null", "a")
28
+ $stderr.reopen("/dev/null", "a")
29
+ end
30
+ $stdin.sync = $stdout.sync = $stderr.sync = true
31
+ end
32
+
33
+ end
@@ -75,7 +75,6 @@ module Unicorn
75
75
  def bind_listen(address = '0.0.0.0:8080', backlog = 1024)
76
76
  return address unless String === address
77
77
 
78
- address = File.expand_path(address) if address[0..0] == "~"
79
78
  domain, bind_addr = if address[0..0] == "/"
80
79
  if File.exist?(address)
81
80
  if File.socket?(address)
@@ -300,7 +300,7 @@ end
300
300
  sleep DEFAULT_RES
301
301
  log = File.readlines(rotate.path)
302
302
  end
303
- assert_equal 4, log.grep(/rotating logs\.\.\./).size
303
+ assert_equal 4, log.grep(/worker=\d+ rotating logs\.\.\./).size
304
304
  assert_equal 0, log.grep(/done rotating logs/).size
305
305
 
306
306
  tries = DEFAULT_TRIES
@@ -309,7 +309,7 @@ end
309
309
  sleep DEFAULT_RES
310
310
  log = File.readlines(COMMON_TMP.path)
311
311
  end
312
- assert_equal 4, log.grep(/done rotating logs/).size
312
+ assert_equal 4, log.grep(/worker=\d+ done rotating logs/).size
313
313
  assert_equal 0, log.grep(/rotating logs\.\.\./).size
314
314
  assert_nothing_raised { Process.kill('QUIT', pid) }
315
315
  status = nil
@@ -25,11 +25,10 @@ class HttpParserTest < Test::Unit::TestCase
25
25
  assert_equal '/', req['REQUEST_PATH']
26
26
  assert_equal 'HTTP/1.1', req['HTTP_VERSION']
27
27
  assert_equal '/', req['REQUEST_URI']
28
- assert_equal 'CGI/1.2', req['GATEWAY_INTERFACE']
29
28
  assert_equal 'GET', req['REQUEST_METHOD']
30
29
  assert_nil req['FRAGMENT']
31
30
  assert_nil req['QUERY_STRING']
32
-
31
+
33
32
  parser.reset
34
33
  assert parser.nread == 0, "Number read after reset should be 0"
35
34
  end
@@ -0,0 +1,82 @@
1
+ # Copyright (c) 2009 Eric Wong
2
+ # You can redistribute it and/or modify it under the same terms as Ruby.
3
+
4
+ if RUBY_VERSION =~ /1\.9/
5
+ warn "#$0 current broken under Ruby 1.9 with Rack"
6
+ exit 0
7
+ end
8
+
9
+ require 'test/test_helper'
10
+ begin
11
+ require 'rack'
12
+ require 'rack/lint'
13
+ rescue LoadError
14
+ warn "Unable to load rack, skipping test"
15
+ exit 0
16
+ end
17
+
18
+ include Unicorn
19
+
20
+ class RequestTest < Test::Unit::TestCase
21
+
22
+ class MockRequest < StringIO
23
+ def unicorn_peeraddr
24
+ '666.666.666.666'
25
+ end
26
+ end
27
+
28
+ def setup
29
+ @request = HttpRequest.new(Logger.new($stderr))
30
+ @app = lambda do |env|
31
+ [ 200, { 'Content-Length' => '0', 'Content-Type' => 'text/plain' }, [] ]
32
+ end
33
+ @lint = Rack::Lint.new(@app)
34
+ end
35
+
36
+ def test_rack_lint_get
37
+ client = MockRequest.new("GET / HTTP/1.1\r\nHost: foo\r\n\r\n")
38
+ res = env = nil
39
+ assert_nothing_raised { env = @request.read(client) }
40
+ assert_equal '666.666.666.666', env['REMOTE_ADDR']
41
+ assert_nothing_raised { res = @lint.call(env) }
42
+ end
43
+
44
+ def test_rack_lint_put
45
+ client = MockRequest.new(
46
+ "PUT / HTTP/1.1\r\n" \
47
+ "Host: foo\r\n" \
48
+ "Content-Length: 5\r\n" \
49
+ "\r\n" \
50
+ "abcde")
51
+ res = env = nil
52
+ assert_nothing_raised { env = @request.read(client) }
53
+ assert_nothing_raised { res = @lint.call(env) }
54
+ end
55
+
56
+ def test_rack_lint_big_put
57
+ count = 100
58
+ bs = 0x10000
59
+ buf = (' ' * bs).freeze
60
+ length = bs * count
61
+ client = Tempfile.new('big_put')
62
+ def client.unicorn_peeraddr
63
+ '1.1.1.1'
64
+ end
65
+ client.syswrite(
66
+ "PUT / HTTP/1.1\r\n" \
67
+ "Host: foo\r\n" \
68
+ "Content-Length: #{length}\r\n" \
69
+ "\r\n")
70
+ count.times { assert_equal bs, client.syswrite(buf) }
71
+ assert_equal 0, client.sysseek(0)
72
+ res = env = nil
73
+ assert_nothing_raised { env = @request.read(client) }
74
+ assert_equal length, env['rack.input'].size
75
+ count.times { assert_equal buf, env['rack.input'].read(bs) }
76
+ assert_nil env['rack.input'].read(bs)
77
+ assert_nothing_raised { env['rack.input'].rewind }
78
+ assert_nothing_raised { res = @lint.call(env) }
79
+ end
80
+
81
+ end
82
+
@@ -41,5 +41,12 @@ class ResponseTest < Test::Unit::TestCase
41
41
  io.rewind
42
42
  assert_match(/.* #{HTTP_STATUS_CODES[code]}$/, io.readline.chomp, "wrong default reason phrase")
43
43
  end
44
+
45
+ def test_rack_multivalue_headers
46
+ out = StringIO.new
47
+ HttpResponse.write(out,[200, {"X-Whatever" => "stuff\nbleh"}, []])
48
+ assert_match(/^X-Whatever: stuff\r\nX-Whatever: bleh\r\n/, out.string)
49
+ end
50
+
44
51
  end
45
52
 
data/unicorn.gemspec CHANGED
@@ -2,17 +2,17 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = %q{unicorn}
5
- s.version = "0.2.1"
5
+ s.version = "0.2.2"
6
6
 
7
7
  s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
8
8
  s.authors = ["Eric Wong"]
9
- s.date = %q{2009-03-18}
9
+ s.date = %q{2009-03-22}
10
10
  s.description = %q{A small fast HTTP library and server for Rack applications.}
11
11
  s.email = %q{normalperson@yhbt.net}
12
12
  s.executables = ["unicorn", "unicorn_rails"]
13
13
  s.extensions = ["ext/unicorn/http11/extconf.rb"]
14
- s.extra_rdoc_files = ["CHANGELOG", "LICENSE", "README", "TODO", "bin/unicorn", "bin/unicorn_rails", "ext/unicorn/http11/ext_help.h", "ext/unicorn/http11/extconf.rb", "ext/unicorn/http11/http11.c", "ext/unicorn/http11/http11_parser.c", "ext/unicorn/http11/http11_parser.h", "ext/unicorn/http11/http11_parser.rl", "ext/unicorn/http11/http11_parser_common.rl", "lib/unicorn.rb", "lib/unicorn/configurator.rb", "lib/unicorn/const.rb", "lib/unicorn/http_request.rb", "lib/unicorn/http_response.rb", "lib/unicorn/socket.rb", "lib/unicorn/util.rb"]
15
- s.files = [".document", ".gitignore", "CHANGELOG", "CONTRIBUTORS", "DESIGN", "GNUmakefile", "LICENSE", "Manifest", "README", "Rakefile", "SIGNALS", "TODO", "bin/unicorn", "bin/unicorn_rails", "ext/unicorn/http11/ext_help.h", "ext/unicorn/http11/extconf.rb", "ext/unicorn/http11/http11.c", "ext/unicorn/http11/http11_parser.c", "ext/unicorn/http11/http11_parser.h", "ext/unicorn/http11/http11_parser.rl", "ext/unicorn/http11/http11_parser_common.rl", "lib/unicorn.rb", "lib/unicorn/configurator.rb", "lib/unicorn/const.rb", "lib/unicorn/http_request.rb", "lib/unicorn/http_response.rb", "lib/unicorn/socket.rb", "lib/unicorn/util.rb", "setup.rb", "test/aggregate.rb", "test/benchmark/previous.rb", "test/benchmark/simple.rb", "test/benchmark/utils.rb", "test/exec/README", "test/exec/test_exec.rb", "test/test_helper.rb", "test/tools/trickletest.rb", "test/unit/test_configurator.rb", "test/unit/test_http_parser.rb", "test/unit/test_response.rb", "test/unit/test_server.rb", "test/unit/test_upload.rb", "unicorn.gemspec"]
14
+ s.extra_rdoc_files = ["CHANGELOG", "LICENSE", "README", "TODO", "bin/unicorn", "bin/unicorn_rails", "ext/unicorn/http11/ext_help.h", "ext/unicorn/http11/extconf.rb", "ext/unicorn/http11/http11.c", "ext/unicorn/http11/http11_parser.c", "ext/unicorn/http11/http11_parser.h", "ext/unicorn/http11/http11_parser.rl", "ext/unicorn/http11/http11_parser_common.rl", "lib/unicorn.rb", "lib/unicorn/app/exec_cgi.rb", "lib/unicorn/configurator.rb", "lib/unicorn/const.rb", "lib/unicorn/http_request.rb", "lib/unicorn/http_response.rb", "lib/unicorn/launcher.rb", "lib/unicorn/socket.rb", "lib/unicorn/util.rb"]
15
+ s.files = [".document", ".gitignore", "CHANGELOG", "CONTRIBUTORS", "DESIGN", "GNUmakefile", "LICENSE", "Manifest", "README", "Rakefile", "SIGNALS", "TODO", "bin/unicorn", "bin/unicorn_rails", "ext/unicorn/http11/ext_help.h", "ext/unicorn/http11/extconf.rb", "ext/unicorn/http11/http11.c", "ext/unicorn/http11/http11_parser.c", "ext/unicorn/http11/http11_parser.h", "ext/unicorn/http11/http11_parser.rl", "ext/unicorn/http11/http11_parser_common.rl", "lib/unicorn.rb", "lib/unicorn/app/exec_cgi.rb", "lib/unicorn/configurator.rb", "lib/unicorn/const.rb", "lib/unicorn/http_request.rb", "lib/unicorn/http_response.rb", "lib/unicorn/launcher.rb", "lib/unicorn/socket.rb", "lib/unicorn/util.rb", "setup.rb", "test/aggregate.rb", "test/benchmark/previous.rb", "test/benchmark/simple.rb", "test/benchmark/utils.rb", "test/exec/README", "test/exec/test_exec.rb", "test/test_helper.rb", "test/tools/trickletest.rb", "test/unit/test_configurator.rb", "test/unit/test_http_parser.rb", "test/unit/test_request.rb", "test/unit/test_response.rb", "test/unit/test_server.rb", "test/unit/test_upload.rb", "unicorn.gemspec"]
16
16
  s.has_rdoc = true
17
17
  s.homepage = %q{http://unicorn.bogomips.org}
18
18
  s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Unicorn", "--main", "README"]
@@ -20,7 +20,7 @@ Gem::Specification.new do |s|
20
20
  s.rubyforge_project = %q{unicorn}
21
21
  s.rubygems_version = %q{1.3.1}
22
22
  s.summary = %q{A small fast HTTP library and server for Rack applications.}
23
- s.test_files = ["test/unit/test_configurator.rb", "test/unit/test_response.rb", "test/unit/test_upload.rb", "test/unit/test_http_parser.rb", "test/unit/test_server.rb"]
23
+ s.test_files = ["test/unit/test_configurator.rb", "test/unit/test_server.rb", "test/unit/test_upload.rb", "test/unit/test_response.rb", "test/unit/test_http_parser.rb", "test/unit/test_request.rb"]
24
24
 
25
25
  if s.respond_to? :specification_version then
26
26
  current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: unicorn
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric Wong
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-03-18 00:00:00 -07:00
12
+ date: 2009-03-22 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
@@ -35,10 +35,12 @@ extra_rdoc_files:
35
35
  - ext/unicorn/http11/http11_parser.rl
36
36
  - ext/unicorn/http11/http11_parser_common.rl
37
37
  - lib/unicorn.rb
38
+ - lib/unicorn/app/exec_cgi.rb
38
39
  - lib/unicorn/configurator.rb
39
40
  - lib/unicorn/const.rb
40
41
  - lib/unicorn/http_request.rb
41
42
  - lib/unicorn/http_response.rb
43
+ - lib/unicorn/launcher.rb
42
44
  - lib/unicorn/socket.rb
43
45
  - lib/unicorn/util.rb
44
46
  files:
@@ -64,10 +66,12 @@ files:
64
66
  - ext/unicorn/http11/http11_parser.rl
65
67
  - ext/unicorn/http11/http11_parser_common.rl
66
68
  - lib/unicorn.rb
69
+ - lib/unicorn/app/exec_cgi.rb
67
70
  - lib/unicorn/configurator.rb
68
71
  - lib/unicorn/const.rb
69
72
  - lib/unicorn/http_request.rb
70
73
  - lib/unicorn/http_response.rb
74
+ - lib/unicorn/launcher.rb
71
75
  - lib/unicorn/socket.rb
72
76
  - lib/unicorn/util.rb
73
77
  - setup.rb
@@ -81,6 +85,7 @@ files:
81
85
  - test/tools/trickletest.rb
82
86
  - test/unit/test_configurator.rb
83
87
  - test/unit/test_http_parser.rb
88
+ - test/unit/test_request.rb
84
89
  - test/unit/test_response.rb
85
90
  - test/unit/test_server.rb
86
91
  - test/unit/test_upload.rb
@@ -119,7 +124,8 @@ specification_version: 2
119
124
  summary: A small fast HTTP library and server for Rack applications.
120
125
  test_files:
121
126
  - test/unit/test_configurator.rb
122
- - test/unit/test_response.rb
127
+ - test/unit/test_server.rb
123
128
  - test/unit/test_upload.rb
129
+ - test/unit/test_response.rb
124
130
  - test/unit/test_http_parser.rb
125
- - test/unit/test_server.rb
131
+ - test/unit/test_request.rb