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 +1 -0
- data/GIT-VERSION-GEN +1 -1
- data/GNUmakefile +10 -5
- data/TODO +0 -6
- data/bin/unicorn +3 -3
- data/ext/unicorn_http/unicorn_http.rl +12 -11
- data/lib/unicorn.rb +62 -34
- data/lib/unicorn/configurator.rb +23 -16
- data/lib/unicorn/const.rb +1 -1
- data/lib/unicorn/tee_input.rb +4 -8
- data/lib/unicorn/util.rb +13 -3
- data/test/exec/test_exec.rb +132 -0
- data/test/test_helper.rb +1 -1
- data/test/unit/test_http_parser_ng.rb +66 -0
- data/test/unit/test_signals.rb +13 -10
- data/test/unit/test_tee_input.rb +1 -1
- metadata +2 -2
data/CONTRIBUTORS
CHANGED
data/GIT-VERSION-GEN
CHANGED
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
|
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
|
-
|
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.
|
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
|
253
|
-
cd pkg && tar
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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->
|
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->
|
373
|
+
hp->offset = p - buffer;
|
373
374
|
|
374
375
|
assert(p <= pe && "buffer overflow after parsing execute");
|
375
|
-
assert(hp->
|
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->
|
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->
|
539
|
-
hp->
|
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->
|
641
|
+
assert(hp->s.dest_offset <= hp->offset &&
|
641
642
|
"destination buffer overflow");
|
642
|
-
advance_str(data, hp->
|
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->
|
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
|
-
|
96
|
-
|
97
|
-
|
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
|
-
|
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
|
-
|
527
|
-
|
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
|
data/lib/unicorn/configurator.rb
CHANGED
@@ -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
|
-
#
|
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
|
-
|
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.
|
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
|
data/lib/unicorn/tee_input.rb
CHANGED
@@ -38,7 +38,7 @@ module Unicorn
|
|
38
38
|
@tmp.seek(pos)
|
39
39
|
end
|
40
40
|
|
41
|
-
@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 =
|
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 =
|
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
|
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
|
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
|
-
|
51
|
-
|
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
|
data/test/exec/test_exec.rb
CHANGED
@@ -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
@@ -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" \
|
data/test/unit/test_signals.rb
CHANGED
@@ -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
|
-
|
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", @
|
35
|
+
:listeners => [ "127.0.0.1:#@port", @sock.path ],
|
35
36
|
:after_fork => lambda { |server,worker|
|
36
|
-
trap(:HUP) { tmp.
|
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
|
-
|
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
|
160
|
-
|
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
|
-
|
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
|
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
|
data/test/unit/test_tee_input.rb
CHANGED
@@ -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 {
|
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.
|
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-
|
12
|
+
date: 2009-11-05 00:00:00 -08:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|