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