unicorn 0.93.5 → 0.94.0

Sign up to get free protection for your applications and to get access to all the features.
data/CONTRIBUTORS CHANGED
@@ -2,6 +2,7 @@ Unicorn developers:
2
2
  * Eric Wong
3
3
  * Suraj N. Kurapati
4
4
  * Andrey Stikheev
5
+ * Wayne Larsen
5
6
  * ... (help wanted)
6
7
 
7
8
  We would like to thank following folks for helping make Unicorn possible:
data/GIT-VERSION-GEN CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/bin/sh
2
2
 
3
3
  GVF=GIT-VERSION-FILE
4
- DEF_VER=v0.93.5.GIT
4
+ DEF_VER=v0.94.0.GIT
5
5
 
6
6
  LF='
7
7
  '
data/GNUmakefile CHANGED
@@ -57,7 +57,8 @@ http: lib/unicorn_http.$(DLEXT)
57
57
 
58
58
  $(test_prefix)/.stamp: $(inst_deps)
59
59
  mkdir -p $(test_prefix)/.ccache
60
- tar c $(inst_deps) GIT-VERSION-GEN | (cd $(test_prefix) && tar x)
60
+ tar cf - $(inst_deps) GIT-VERSION-GEN | \
61
+ (cd $(test_prefix) && tar xf -)
61
62
  $(MAKE) -C $(test_prefix) clean
62
63
  $(MAKE) -C $(test_prefix) http shebang
63
64
  > $@
@@ -91,9 +92,13 @@ else
91
92
  rm $(stamp) 2>/dev/null && $(check_test)
92
93
  endif
93
94
 
95
+ # not all systems have setsid(8), we need it because we spam signals
96
+ # stupidly in some tests...
97
+ rb_setsid := $(ruby) -e 'Process.setsid' -e 'exec *ARGV'
98
+
94
99
  # TRACER='strace -f -o $(t).strace -s 100000'
95
100
  run_test = $(quiet_pre) \
96
- setsid $(TRACER) $(ruby) -w $(arg) $(TEST_OPTS) $(quiet_post) || \
101
+ $(rb_setsid) $(TRACER) $(ruby) -w $(arg) $(TEST_OPTS) $(quiet_post) || \
97
102
  (sed "s,^,$(extra): ," >&2 < $(t); exit 1)
98
103
 
99
104
  %.n: arg = $(subst .n,,$(subst --, -n ,$@))
@@ -151,7 +156,7 @@ NEWS: GIT-VERSION-FILE
151
156
  $(rake) -s news_rdoc > $@+
152
157
  mv $@+ $@
153
158
 
154
- SINCE = 0.92.0
159
+ SINCE = 0.93.0
155
160
  ChangeLog: log_range = $(shell test -n "$(SINCE)" && echo v$(SINCE)..)
156
161
  ChangeLog: GIT-VERSION-FILE
157
162
  @echo "ChangeLog from $(GIT_URL) ($(SINCE)..$(GIT_VERSION))" > $@+
@@ -249,8 +254,8 @@ $(pkgtgz): manifest fix-perms
249
254
  @test -n "$(distdir)"
250
255
  $(RM) -r $(distdir)
251
256
  mkdir -p $(distdir)
252
- tar c `cat .manifest` | (cd $(distdir) && tar x)
253
- cd pkg && tar c $(basename $(@F)) | gzip -9 > $(@F)+
257
+ tar cf - `cat .manifest` | (cd $(distdir) && tar xf -)
258
+ cd pkg && tar cf - $(basename $(@F)) | gzip -9 > $(@F)+
254
259
  mv $@+ $@
255
260
 
256
261
  package: $(pkgtgz) $(pkggem)
data/TODO CHANGED
@@ -3,12 +3,6 @@
3
3
  * ensure test suite passes on non-GNU/Linux systems
4
4
  (likely that it already does)
5
5
 
6
- * consider adding "working_directory" directive to Configurator
7
- since START_CTX is ugly...
8
-
9
- * consider adding user switching support (ugh...)
10
- This makes more sense for Rainbows!, but some folks use it already...
11
-
12
6
  * fix const-correctness in HTTP parser
13
7
 
14
8
  * performance validation (esp. TeeInput)
data/bin/unicorn CHANGED
@@ -3,7 +3,7 @@
3
3
  require 'unicorn/launcher'
4
4
  require 'optparse'
5
5
 
6
- env = "development"
6
+ ENV["RACK_ENV"] ||= "development"
7
7
  daemonize = false
8
8
  listeners = []
9
9
  options = { :listeners => listeners }
@@ -58,7 +58,7 @@ opts = OptionParser.new("", 24, ' ') do |opts|
58
58
 
59
59
  opts.on("-E", "--env ENVIRONMENT",
60
60
  "use ENVIRONMENT for defaults (default: development)") do |e|
61
- env = e
61
+ ENV["RACK_ENV"] = e
62
62
  end
63
63
 
64
64
  opts.on("-D", "--daemonize", "run daemonized in the background") do |d|
@@ -133,7 +133,7 @@ app = lambda do ||
133
133
  Object.const_get(File.basename(config, '.rb').capitalize)
134
134
  end
135
135
  pp({ :inner_app => inner_app }) if $DEBUG
136
- case env
136
+ case ENV["RACK_ENV"]
137
137
  when "development"
138
138
  Rack::Builder.new do
139
139
  use Rack::CommonLogger, $stderr
@@ -25,14 +25,15 @@
25
25
  /* both of these flags need to be set for keepalive to be supported */
26
26
  #define UH_FL_KEEPALIVE (UH_FL_KAMETHOD | UH_FL_KAVERSION)
27
27
 
28
+ /* keep this small for Rainbows! since every client has one */
28
29
  struct http_parser {
29
30
  int cs; /* Ragel internal state */
30
31
  unsigned int flags;
31
32
  size_t mark;
32
- union { /* these 3 fields don't nest */
33
+ size_t offset;
34
+ union { /* these 2 fields don't nest */
33
35
  size_t field;
34
36
  size_t query;
35
- size_t offset;
36
37
  } start;
37
38
  union {
38
39
  size_t field_len; /* only used during header processing */
@@ -349,7 +350,7 @@ static void http_parser_execute(struct http_parser *hp,
349
350
  {
350
351
  const char *p, *pe;
351
352
  int cs = hp->cs;
352
- size_t off = hp->start.offset;
353
+ size_t off = hp->offset;
353
354
 
354
355
  if (cs == http_parser_first_final)
355
356
  return;
@@ -369,10 +370,10 @@ static void http_parser_execute(struct http_parser *hp,
369
370
  post_exec: /* "_out:" also goes here */
370
371
  if (hp->cs != http_parser_error)
371
372
  hp->cs = cs;
372
- hp->start.offset = p - buffer;
373
+ hp->offset = p - buffer;
373
374
 
374
375
  assert(p <= pe && "buffer overflow after parsing execute");
375
- assert(hp->start.offset <= len && "start.offset longer than length");
376
+ assert(hp->offset <= len && "offset longer than length");
376
377
  }
377
378
 
378
379
  static struct http_parser *data_get(VALUE self)
@@ -531,12 +532,12 @@ static VALUE HttpParser_headers(VALUE self, VALUE req, VALUE data)
531
532
  rb_str_update(data);
532
533
 
533
534
  http_parser_execute(hp, req, RSTRING_PTR(data), RSTRING_LEN(data));
534
- VALIDATE_MAX_LENGTH(hp->start.offset, HEADER);
535
+ VALIDATE_MAX_LENGTH(hp->offset, HEADER);
535
536
 
536
537
  if (hp->cs == http_parser_first_final ||
537
538
  hp->cs == http_parser_en_ChunkedBody) {
538
- advance_str(data, hp->start.offset + 1);
539
- hp->start.offset = 0;
539
+ advance_str(data, hp->offset + 1);
540
+ hp->offset = 0;
540
541
 
541
542
  return req;
542
543
  }
@@ -637,9 +638,9 @@ static VALUE HttpParser_filter_body(VALUE self, VALUE buf, VALUE data)
637
638
  if (hp->cs == http_parser_error)
638
639
  rb_raise(eHttpParserError, "Invalid HTTP format, parsing fails.");
639
640
 
640
- assert(hp->s.dest_offset <= hp->start.offset &&
641
+ assert(hp->s.dest_offset <= hp->offset &&
641
642
  "destination buffer overflow");
642
- advance_str(data, hp->start.offset);
643
+ advance_str(data, hp->offset);
643
644
  rb_str_set_len(buf, hp->s.dest_offset);
644
645
 
645
646
  if (RSTRING_LEN(buf) == 0 && chunked_eof(hp)) {
@@ -663,7 +664,7 @@ static VALUE HttpParser_filter_body(VALUE self, VALUE buf, VALUE data)
663
664
  data = Qnil;
664
665
  }
665
666
  }
666
- hp->start.offset = 0; /* for trailer parsing */
667
+ hp->offset = 0; /* for trailer parsing */
667
668
  return data;
668
669
  }
669
670
 
data/lib/unicorn.rb CHANGED
@@ -92,9 +92,17 @@ module Unicorn
92
92
  # Unicorn::HttpServer::START_CTX[0] = "/home/bofh/1.9.2/bin/unicorn"
93
93
  START_CTX = {
94
94
  :argv => ARGV.map { |arg| arg.dup },
95
- # don't rely on Dir.pwd here since it's not symlink-aware, and
96
- # symlink dirs are the default with Capistrano...
97
- :cwd => `/bin/sh -c pwd`.chomp("\n"),
95
+ :cwd => lambda {
96
+ # favor ENV['PWD'] since it is (usually) symlink aware for
97
+ # Capistrano and like systems
98
+ begin
99
+ a = File.stat(pwd = ENV['PWD'])
100
+ b = File.stat(Dir.pwd)
101
+ a.ino == b.ino && a.dev == b.dev ? pwd : Dir.pwd
102
+ rescue
103
+ Dir.pwd
104
+ end
105
+ }.call,
98
106
  0 => $0.dup,
99
107
  }
100
108
 
@@ -105,10 +113,37 @@ module Unicorn
105
113
  # for examples.
106
114
  class Worker < Struct.new(:nr, :tmp)
107
115
 
116
+ autoload :Etc, 'etc'
117
+
108
118
  # worker objects may be compared to just plain numbers
109
119
  def ==(other_nr)
110
120
  self.nr == other_nr
111
121
  end
122
+
123
+ # Changes the worker process to the specified +user+ and +group+
124
+ # This is only intended to be called from within the worker
125
+ # process from the +after_fork+ hook. This should be called in
126
+ # the +after_fork+ hook after any priviledged functions need to be
127
+ # run (e.g. to set per-worker CPU affinity, niceness, etc)
128
+ #
129
+ # Any and all errors raised within this method will be propagated
130
+ # directly back to the caller (usually the +after_fork+ hook.
131
+ # These errors commonly include ArgumentError for specifying an
132
+ # invalid user/group and Errno::EPERM for insufficient priviledges
133
+ def user(user, group = nil)
134
+ # we do not protect the caller, checking Process.euid == 0 is
135
+ # insufficient because modern systems have fine-grained
136
+ # capabilities. Let the caller handle any and all errors.
137
+ uid = Etc.getpwnam(user).uid
138
+ gid = Etc.getgrnam(group).gid if group
139
+ tmp.chown(uid, gid)
140
+ if gid && Process.egid != gid
141
+ Process.initgroups(user, gid)
142
+ Process::GID.change_privilege(gid)
143
+ end
144
+ Process.euid != uid and Process::UID.change_privilege(uid)
145
+ end
146
+
112
147
  end
113
148
 
114
149
  # Creates a working server on host:port (strange things happen if
@@ -471,16 +506,8 @@ module Unicorn
471
506
  # is stale for >timeout seconds, then we'll kill the corresponding
472
507
  # worker.
473
508
  def murder_lazy_workers
474
- diff = stat = nil
475
509
  WORKERS.dup.each_pair do |wpid, worker|
476
- stat = begin
477
- worker.tmp.stat
478
- rescue => e
479
- logger.warn "worker=#{worker.nr} PID:#{wpid} stat error: #{e.inspect}"
480
- kill_worker(:QUIT, wpid)
481
- next
482
- end
483
- (diff = (Time.now - stat.ctime)) <= timeout and next
510
+ (diff = (Time.now - worker.tmp.stat.ctime)) <= timeout and next
484
511
  logger.error "worker=#{worker.nr} PID:#{wpid} timeout " \
485
512
  "(#{diff}s > #{timeout}s), killing"
486
513
  kill_worker(:KILL, wpid) # take no prisoners for timeout violations
@@ -490,13 +517,6 @@ module Unicorn
490
517
  def spawn_missing_workers
491
518
  (0...worker_processes).each do |worker_nr|
492
519
  WORKERS.values.include?(worker_nr) and next
493
- begin
494
- Dir.chdir(START_CTX[:cwd])
495
- rescue Errno::ENOENT => err
496
- logger.fatal "#{err.inspect} (#{START_CTX[:cwd]})"
497
- SIG_QUEUE << :QUIT # forcibly emulate SIGQUIT
498
- return
499
- end
500
520
  worker = Worker.new(worker_nr, Unicorn::Util.tmpio)
501
521
  before_fork.call(self, worker)
502
522
  WORKERS[fork { worker_loop(worker) }] = worker
@@ -511,6 +531,27 @@ module Unicorn
511
531
  }
512
532
  end
513
533
 
534
+ # if we get any error, try to write something back to the client
535
+ # assuming we haven't closed the socket, but don't get hung up
536
+ # if the socket is already closed or broken. We'll always ensure
537
+ # the socket is closed at the end of this function
538
+ def handle_error(client, e)
539
+ msg = case e
540
+ when EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL,Errno::EBADF
541
+ Const::ERROR_500_RESPONSE
542
+ when HttpParserError # try to tell the client they're bad
543
+ Const::ERROR_400_RESPONSE
544
+ else
545
+ logger.error "Read error: #{e.inspect}"
546
+ logger.error e.backtrace.join("\n")
547
+ Const::ERROR_500_RESPONSE
548
+ end
549
+ client.write_nonblock(msg)
550
+ client.close
551
+ rescue
552
+ nil
553
+ end
554
+
514
555
  # once a client is accepted, it is processed in its entirety here
515
556
  # in 3 easy steps: read request, call app, write app response
516
557
  def process_client(client)
@@ -523,21 +564,8 @@ module Unicorn
523
564
  response = app.call(env)
524
565
  end
525
566
  HttpResponse.write(client, response, HttpRequest::PARSER.headers?)
526
- # if we get any error, try to write something back to the client
527
- # assuming we haven't closed the socket, but don't get hung up
528
- # if the socket is already closed or broken. We'll always ensure
529
- # the socket is closed at the end of this function
530
- rescue EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL,Errno::EBADF
531
- client.write_nonblock(Const::ERROR_500_RESPONSE) rescue nil
532
- client.close rescue nil
533
- rescue HttpParserError # try to tell the client they're bad
534
- client.write_nonblock(Const::ERROR_400_RESPONSE) rescue nil
535
- client.close rescue nil
536
- rescue Object => e
537
- client.write_nonblock(Const::ERROR_500_RESPONSE) rescue nil
538
- client.close rescue nil
539
- logger.error "Read error: #{e.inspect}"
540
- logger.error e.backtrace.join("\n")
567
+ rescue => e
568
+ handle_error(client, e)
541
569
  end
542
570
 
543
571
  # gets rid of stuff the worker has no business keeping track of
@@ -9,6 +9,7 @@ module Unicorn
9
9
  #
10
10
  # Example (when used with the unicorn config file):
11
11
  # worker_processes 4
12
+ # working_directory "/path/to/deploy/app/current"
12
13
  # listen '/tmp/my_app.sock', :backlog => 1
13
14
  # listen 9292, :tcp_nopush => true
14
15
  # timeout 10
@@ -93,6 +94,15 @@ module Unicorn
93
94
 
94
95
  def reload #:nodoc:
95
96
  instance_eval(File.read(config_file), config_file) if config_file
97
+
98
+ # working_directory binds immediately (easier error checking that way),
99
+ # now ensure any paths we changed are correctly set.
100
+ [ :pid, :stderr_path, :stdout_path ].each do |var|
101
+ String === (path = set[var]) or next
102
+ path = File.expand_path(path)
103
+ test(?w, path) || test(?w, File.dirname(path)) or \
104
+ raise ArgumentError, "directory for #{var}=#{path} not writable"
105
+ end
96
106
  end
97
107
 
98
108
  def commit!(server, options = {}) #:nodoc:
@@ -137,16 +147,7 @@ module Unicorn
137
147
  # # drop permissions to "www-data" in the worker
138
148
  # # generally there's no reason to start Unicorn as a priviledged user
139
149
  # # as it is not recommended to expose Unicorn to public clients.
140
- # uid, gid = Process.euid, Process.egid
141
- # user, group = 'www-data', 'www-data'
142
- # target_uid = Etc.getpwnam(user).uid
143
- # target_gid = Etc.getgrnam(group).gid
144
- # worker.tmp.chown(target_uid, target_gid)
145
- # if uid != target_uid || gid != target_gid
146
- # Process.initgroups(user, target_gid)
147
- # Process::GID.change_privilege(target_gid)
148
- # Process::UID.change_privilege(target_uid)
149
- # end
150
+ # worker.user('www-data', 'www-data') if Process.euid == 0
150
151
  # end
151
152
  def after_fork(*args, &block)
152
153
  set_hook(:after_fork, block_given? ? block : args[0])
@@ -345,6 +346,16 @@ module Unicorn
345
346
  set_path(:stdout_path, path)
346
347
  end
347
348
 
349
+ # sets the working directory for Unicorn. This ensures USR2 will
350
+ # start a new instance of Unicorn in this directory. This may be
351
+ # a symlink.
352
+ def working_directory(path)
353
+ # just let chdir raise errors
354
+ path = File.expand_path(path)
355
+ Dir.chdir(path)
356
+ HttpServer::START_CTX[:cwd] = ENV["PWD"] = path
357
+ end
358
+
348
359
  # expands "unix:path/to/foo" to a socket relative to the current path
349
360
  # expands pathnames of sockets if relative to "~" or "~username"
350
361
  # expands "*:port and ":port" to "0.0.0.0:port"
@@ -372,15 +383,11 @@ module Unicorn
372
383
 
373
384
  def set_path(var, path) #:nodoc:
374
385
  case path
375
- when NilClass
376
- when String
377
- path = File.expand_path(path)
378
- File.writable?(File.dirname(path)) or \
379
- raise ArgumentError, "directory for #{var}=#{path} not writable"
386
+ when NilClass, String
387
+ set[var] = path
380
388
  else
381
389
  raise ArgumentError
382
390
  end
383
- set[var] = path
384
391
  end
385
392
 
386
393
  def set_hook(var, my_proc, req_arity = 2) #:nodoc:
data/lib/unicorn/const.rb CHANGED
@@ -7,7 +7,7 @@ module Unicorn
7
7
  # gave about a 3% to 10% performance improvement over using the strings directly.
8
8
  # Symbols did not really improve things much compared to constants.
9
9
  module Const
10
- UNICORN_VERSION="0.93.5"
10
+ UNICORN_VERSION="0.94.0"
11
11
 
12
12
  DEFAULT_HOST = "0.0.0.0" # default TCP listen host address
13
13
  DEFAULT_PORT = 8080 # default TCP listen port
@@ -38,7 +38,7 @@ module Unicorn
38
38
  @tmp.seek(pos)
39
39
  end
40
40
 
41
- @size = tmp_size
41
+ @size = @tmp.size
42
42
  end
43
43
 
44
44
  # call-seq:
@@ -73,7 +73,7 @@ module Unicorn
73
73
  rv
74
74
  else
75
75
  rv = args.shift || @buf2.dup
76
- diff = tmp_size - @tmp.pos
76
+ diff = @tmp.size - @tmp.pos
77
77
  if 0 == diff
78
78
  ensure_length(tee(length, rv), length)
79
79
  else
@@ -87,7 +87,7 @@ module Unicorn
87
87
  socket or return @tmp.gets
88
88
  nil == $/ and return read
89
89
 
90
- orig_size = tmp_size
90
+ orig_size = @tmp.size
91
91
  if @tmp.pos == orig_size
92
92
  tee(Const::CHUNK_SIZE, @buf2) or return nil
93
93
  @tmp.seek(orig_size)
@@ -142,15 +142,11 @@ module Unicorn
142
142
 
143
143
  def finalize_input
144
144
  while parser.trailers(req, buf).nil?
145
- buf << socket.readpartial(Const::CHUNK_SIZE, @buf2)
145
+ buf << socket.readpartial(Const::CHUNK_SIZE)
146
146
  end
147
147
  self.socket = nil
148
148
  end
149
149
 
150
- def tmp_size
151
- StringIO === @tmp ? @tmp.size : @tmp.stat.size
152
- end
153
-
154
150
  # tee()s into +buf+ until it is of +length+ bytes (or until
155
151
  # we've reached the Content-Length of the request body).
156
152
  # Returns +buf+ (the exact object, not a duplicate)
data/lib/unicorn/util.rb CHANGED
@@ -4,6 +4,16 @@ require 'fcntl'
4
4
  require 'tmpdir'
5
5
 
6
6
  module Unicorn
7
+
8
+ class TmpIO < ::File
9
+
10
+ # for easier env["rack.input"] compatibility
11
+ def size
12
+ # flush if sync
13
+ stat.size
14
+ end
15
+ end
16
+
7
17
  class Util
8
18
  class << self
9
19
 
@@ -21,7 +31,7 @@ module Unicorn
21
31
 
22
32
  ObjectSpace.each_object(File) do |fp|
23
33
  next if fp.closed?
24
- next unless (fp.sync && fp.path[0..0] == "/")
34
+ next unless (fp.sync && fp.path[0] == ?/)
25
35
  next unless (fp.fcntl(Fcntl::F_GETFL) & append_flags) == append_flags
26
36
 
27
37
  begin
@@ -47,8 +57,8 @@ module Unicorn
47
57
  # buffering is disabled
48
58
  def tmpio
49
59
  fp = begin
50
- File.open("#{Dir::tmpdir}/#{rand}",
51
- File::RDWR|File::CREAT|File::EXCL, 0600)
60
+ TmpIO.open("#{Dir::tmpdir}/#{rand}",
61
+ File::RDWR|File::CREAT|File::EXCL, 0600)
52
62
  rescue Errno::EEXIST
53
63
  retry
54
64
  end
@@ -28,6 +28,13 @@ use Rack::ContentLength
28
28
  run proc { |env| [ 200, { 'Content-Type' => 'text/plain' }, [ "HI\\n" ] ] }
29
29
  EOS
30
30
 
31
+ SHOW_RACK_ENV = <<-EOS
32
+ use Rack::ContentLength
33
+ run proc { |env|
34
+ [ 200, { 'Content-Type' => 'text/plain' }, [ ENV['RACK_ENV'] ] ]
35
+ }
36
+ EOS
37
+
31
38
  HELLO = <<-EOS
32
39
  class Hello
33
40
  def call(env)
@@ -75,6 +82,91 @@ end
75
82
  end
76
83
  end
77
84
 
85
+ def test_working_directory
86
+ other = Tempfile.new('unicorn.wd')
87
+ File.unlink(other.path)
88
+ Dir.mkdir(other.path)
89
+ File.open("config.ru", "wb") do |fp|
90
+ fp.syswrite <<EOF
91
+ use Rack::ContentLength
92
+ run proc { |env| [ 200, { 'Content-Type' => 'text/plain' }, [ Dir.pwd ] ] }
93
+ EOF
94
+ end
95
+ FileUtils.cp("config.ru", other.path + "/config.ru")
96
+ tmp = Tempfile.new('unicorn.config')
97
+ tmp.syswrite <<EOF
98
+ working_directory '#@tmpdir'
99
+ listen '#@addr:#@port'
100
+ EOF
101
+ pid = xfork { redirect_test_io { exec($unicorn_bin, "-c#{tmp.path}") } }
102
+ wait_workers_ready("test_stderr.#{pid}.log", 1)
103
+ results = hit(["http://#@addr:#@port/"])
104
+ assert_equal @tmpdir, results.first
105
+ File.truncate("test_stderr.#{pid}.log", 0)
106
+
107
+ tmp.sysseek(0)
108
+ tmp.truncate(0)
109
+ tmp.syswrite <<EOF
110
+ working_directory '#{other.path}'
111
+ listen '#@addr:#@port'
112
+ EOF
113
+
114
+ Process.kill(:HUP, pid)
115
+ wait_workers_ready("test_stderr.#{pid}.log", 1)
116
+ results = hit(["http://#@addr:#@port/"])
117
+ assert_equal other.path, results.first
118
+
119
+ Process.kill(:QUIT, pid)
120
+ ensure
121
+ FileUtils.rmtree(other.path)
122
+ end
123
+
124
+ def test_working_directory_controls_relative_paths
125
+ other = Tempfile.new('unicorn.wd')
126
+ File.unlink(other.path)
127
+ Dir.mkdir(other.path)
128
+ File.open("config.ru", "wb") do |fp|
129
+ fp.syswrite <<EOF
130
+ use Rack::ContentLength
131
+ run proc { |env| [ 200, { 'Content-Type' => 'text/plain' }, [ Dir.pwd ] ] }
132
+ EOF
133
+ end
134
+ FileUtils.cp("config.ru", other.path + "/config.ru")
135
+ system('mkfifo', "#{other.path}/fifo")
136
+ tmp = Tempfile.new('unicorn.config')
137
+ tmp.syswrite <<EOF
138
+ pid "pid_file_here"
139
+ stderr_path "stderr_log_here"
140
+ stdout_path "stdout_log_here"
141
+ working_directory '#{other.path}'
142
+ listen '#@addr:#@port'
143
+ after_fork do |server, worker|
144
+ File.open("fifo", "wb").close
145
+ end
146
+ EOF
147
+ pid = xfork { redirect_test_io { exec($unicorn_bin, "-c#{tmp.path}") } }
148
+ File.open("#{other.path}/fifo", "rb").close
149
+
150
+ assert ! File.exist?("stderr_log_here")
151
+ assert ! File.exist?("stdout_log_here")
152
+ assert ! File.exist?("pid_file_here")
153
+
154
+ assert ! File.exist?("#@tmpdir/stderr_log_here")
155
+ assert ! File.exist?("#@tmpdir/stdout_log_here")
156
+ assert ! File.exist?("#@tmpdir/pid_file_here")
157
+
158
+ assert File.exist?("#{other.path}/pid_file_here")
159
+ assert_equal "#{pid}\n", File.read("#{other.path}/pid_file_here")
160
+ assert File.exist?("#{other.path}/stderr_log_here")
161
+ assert File.exist?("#{other.path}/stdout_log_here")
162
+ wait_master_ready("#{other.path}/stderr_log_here")
163
+
164
+ Process.kill(:QUIT, pid)
165
+ ensure
166
+ FileUtils.rmtree(other.path)
167
+ end
168
+
169
+
78
170
  def test_exit_signals
79
171
  %w(INT TERM QUIT).each do |sig|
80
172
  File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
@@ -102,6 +194,46 @@ end
102
194
  assert_shutdown(pid)
103
195
  end
104
196
 
197
+ def test_rack_env_unset
198
+ File.open("config.ru", "wb") { |fp| fp.syswrite(SHOW_RACK_ENV) }
199
+ pid = fork { redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port") } }
200
+ results = retry_hit(["http://#{@addr}:#{@port}/"])
201
+ assert_equal "development", results.first
202
+ assert_shutdown(pid)
203
+ end
204
+
205
+ def test_rack_env_cli_set
206
+ File.open("config.ru", "wb") { |fp| fp.syswrite(SHOW_RACK_ENV) }
207
+ pid = fork {
208
+ redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port", "-Easdf") }
209
+ }
210
+ results = retry_hit(["http://#{@addr}:#{@port}/"])
211
+ assert_equal "asdf", results.first
212
+ assert_shutdown(pid)
213
+ end
214
+
215
+ def test_rack_env_ENV_set
216
+ File.open("config.ru", "wb") { |fp| fp.syswrite(SHOW_RACK_ENV) }
217
+ pid = fork {
218
+ ENV["RACK_ENV"] = "foobar"
219
+ redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port") }
220
+ }
221
+ results = retry_hit(["http://#{@addr}:#{@port}/"])
222
+ assert_equal "foobar", results.first
223
+ assert_shutdown(pid)
224
+ end
225
+
226
+ def test_rack_env_cli_override_ENV
227
+ File.open("config.ru", "wb") { |fp| fp.syswrite(SHOW_RACK_ENV) }
228
+ pid = fork {
229
+ ENV["RACK_ENV"] = "foobar"
230
+ redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port", "-Easdf") }
231
+ }
232
+ results = retry_hit(["http://#{@addr}:#{@port}/"])
233
+ assert_equal "asdf", results.first
234
+ assert_shutdown(pid)
235
+ end
236
+
105
237
  def test_ttin_ttou
106
238
  File.open("config.ru", "wb") { |fp| fp.syswrite(HI) }
107
239
  pid = fork { redirect_test_io { exec($unicorn_bin, "-l#@addr:#@port") } }
data/test/test_helper.rb CHANGED
@@ -145,7 +145,7 @@ def retry_hit(uris = [])
145
145
  tries = DEFAULT_TRIES
146
146
  begin
147
147
  hit(uris)
148
- rescue Errno::ECONNREFUSED => err
148
+ rescue Errno::EINVAL, Errno::ECONNREFUSED => err
149
149
  if (tries -= 1) > 0
150
150
  sleep DEFAULT_RES
151
151
  retry
@@ -12,6 +12,24 @@ class HttpParserNgTest < Test::Unit::TestCase
12
12
  @parser = HttpParser.new
13
13
  end
14
14
 
15
+ def test_identity_byte_headers
16
+ req = {}
17
+ str = "PUT / HTTP/1.1\r\n"
18
+ str << "Content-Length: 123\r\n"
19
+ str << "\r"
20
+ hdr = ""
21
+ str.each_byte { |byte|
22
+ assert_nil @parser.headers(req, hdr << byte.chr)
23
+ }
24
+ hdr << "\n"
25
+ assert_equal req.object_id, @parser.headers(req, hdr).object_id
26
+ assert_equal '123', req['CONTENT_LENGTH']
27
+ assert_equal 0, hdr.size
28
+ assert ! @parser.keepalive?
29
+ assert @parser.headers?
30
+ assert 123, @parser.content_length
31
+ end
32
+
15
33
  def test_identity_step_headers
16
34
  req = {}
17
35
  str = "PUT / HTTP/1.1\r\n"
@@ -172,6 +190,26 @@ class HttpParserNgTest < Test::Unit::TestCase
172
190
  assert ! @parser.keepalive?
173
191
  end
174
192
 
193
+ def test_chunks_bytewise
194
+ chunked = "10\r\nabcdefghijklmnop\r\n11\r\n0123456789abcdefg\r\n0\r\n"
195
+ str = "PUT / HTTP/1.1\r\ntransfer-Encoding: chunked\r\n\r\n#{chunked}"
196
+ req = {}
197
+ assert_equal req, @parser.headers(req, str)
198
+ assert_equal chunked, str
199
+ tmp = ''
200
+ buf = ''
201
+ body = ''
202
+ str = str[0..-2]
203
+ str.each_byte { |byte|
204
+ assert_nil @parser.filter_body(tmp, buf << byte.chr)
205
+ body << tmp
206
+ }
207
+ assert_equal 'abcdefghijklmnop0123456789abcdefg', body
208
+ rv = @parser.filter_body(tmp, buf << "\n")
209
+ assert_equal rv.object_id, buf.object_id
210
+ assert ! @parser.keepalive?
211
+ end
212
+
175
213
  def test_trailers
176
214
  str = "PUT / HTTP/1.1\r\n" \
177
215
  "Trailer: Content-MD5\r\n" \
@@ -199,6 +237,34 @@ class HttpParserNgTest < Test::Unit::TestCase
199
237
  assert ! @parser.keepalive?
200
238
  end
201
239
 
240
+ def test_trailers_slowly
241
+ str = "PUT / HTTP/1.1\r\n" \
242
+ "Trailer: Content-MD5\r\n" \
243
+ "transfer-Encoding: chunked\r\n\r\n" \
244
+ "1\r\na\r\n2\r\n..\r\n0\r\n"
245
+ req = {}
246
+ assert_equal req, @parser.headers(req, str)
247
+ assert_equal 'Content-MD5', req['HTTP_TRAILER']
248
+ assert_nil req['HTTP_CONTENT_MD5']
249
+ tmp = ''
250
+ assert_nil @parser.filter_body(tmp, str)
251
+ assert_equal 'a..', tmp
252
+ md5_b64 = [ Digest::MD5.digest(tmp) ].pack('m').strip.freeze
253
+ rv = @parser.filter_body(tmp, str)
254
+ assert_equal rv.object_id, str.object_id
255
+ assert_equal '', str
256
+ assert_nil @parser.trailers(req, str)
257
+ md5_hdr = "Content-MD5: #{md5_b64}\r\n".freeze
258
+ md5_hdr.each_byte { |byte|
259
+ str << byte.chr
260
+ assert_nil @parser.trailers(req, str)
261
+ }
262
+ assert_equal md5_b64, req['HTTP_CONTENT_MD5']
263
+ assert_equal "CONTENT_MD5: #{md5_b64}\r\n", str
264
+ assert_nil @parser.trailers(req, str << "\r")
265
+ assert_equal req, @parser.trailers(req, str << "\n")
266
+ end
267
+
202
268
  def test_max_chunk
203
269
  str = "PUT / HTTP/1.1\r\n" \
204
270
  "transfer-Encoding: chunked\r\n\r\n" \
@@ -26,14 +26,15 @@ class SignalsTest < Test::Unit::TestCase
26
26
  @bs = 1 * 1024 * 1024
27
27
  @count = 100
28
28
  @port = unused_port
29
- tmp = @tmp = Tempfile.new('unicorn.sock')
29
+ @sock = Tempfile.new('unicorn.sock')
30
+ @tmp = Tempfile.new('unicorn.write')
31
+ @tmp.sync = true
32
+ File.unlink(@sock.path)
30
33
  File.unlink(@tmp.path)
31
- n = 0
32
- tmp.chmod(0)
33
34
  @server_opts = {
34
- :listeners => [ "127.0.0.1:#@port", @tmp.path ],
35
+ :listeners => [ "127.0.0.1:#@port", @sock.path ],
35
36
  :after_fork => lambda { |server,worker|
36
- trap(:HUP) { tmp.chmod(n += 1) }
37
+ trap(:HUP) { @tmp.syswrite('.') }
37
38
  },
38
39
  }
39
40
  @server = nil
@@ -143,7 +144,7 @@ class SignalsTest < Test::Unit::TestCase
143
144
  end
144
145
  assert pid > 0, "pid not positive: #{pid.inspect}"
145
146
  read = buf.size
146
- mode_before = @tmp.stat.mode
147
+ size_before = @tmp.stat.size
147
148
  assert_raises(EOFError,Errno::ECONNRESET,Errno::EPIPE,Errno::EINVAL,
148
149
  Errno::EBADF) do
149
150
  loop do
@@ -156,8 +157,10 @@ class SignalsTest < Test::Unit::TestCase
156
157
 
157
158
  redirect_test_io { @server.stop(true) }
158
159
  # can't check for == since pending signals get merged
159
- assert mode_before < @tmp.stat.mode
160
- assert_equal(read - header_len, @bs * @count)
160
+ assert size_before < @tmp.stat.size
161
+ got = read - header_len
162
+ expect = @bs * @count
163
+ assert_equal(expect, got, "expect=#{expect} got=#{got}")
161
164
  assert_nothing_raised { sock.close }
162
165
  end
163
166
 
@@ -183,7 +186,7 @@ class SignalsTest < Test::Unit::TestCase
183
186
  sock.syswrite("PUT / HTTP/1.0\r\n")
184
187
  sock.syswrite("Content-Length: #{@bs * @count}\r\n\r\n")
185
188
  1000.times { Process.kill(:HUP, pid) }
186
- mode_before = @tmp.stat.mode
189
+ size_before = @tmp.stat.size
187
190
  killer = fork { loop { Process.kill(:HUP, pid); sleep(0.0001) } }
188
191
  buf = ' ' * @bs
189
192
  @count.times { sock.syswrite(buf) }
@@ -191,7 +194,7 @@ class SignalsTest < Test::Unit::TestCase
191
194
  Process.waitpid2(killer)
192
195
  redirect_test_io { @server.stop(true) }
193
196
  # can't check for == since pending signals get merged
194
- assert mode_before < @tmp.stat.mode
197
+ assert size_before < @tmp.stat.size
195
198
  assert_equal pid, sock.sysread(4096)[/\r\nX-Pid: (\d+)\r\n/, 1].to_i
196
199
  sock.close
197
200
  end
@@ -127,7 +127,7 @@ class TestTeeInput < Test::Unit::TestCase
127
127
  assert ! @parser.body_eof?
128
128
  assert_kind_of File, ti.instance_eval { @tmp }
129
129
  assert_equal 0, ti.instance_eval { @tmp.pos }
130
- assert_equal 1, ti.instance_eval { tmp_size }
130
+ assert_equal 1, ti.instance_eval { @tmp.size }
131
131
  assert_equal Unicorn::Const::MAX_BODY + 1, ti.size
132
132
  nr = Unicorn::Const::MAX_BODY / 4
133
133
  pid = fork {
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.93.5
4
+ version: 0.94.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Unicorn developers
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-10-29 00:00:00 -07:00
12
+ date: 2009-11-05 00:00:00 -08:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency