unicorn 0.93.5 → 0.94.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/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