unicorn 0.8.4 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +1 -0
- data/CHANGELOG +1 -3
- data/COPYING +339 -0
- data/GNUmakefile +12 -8
- data/LICENSE +3 -3
- data/Manifest +9 -0
- data/README +20 -8
- data/TODO +5 -13
- data/examples/echo.ru +32 -0
- data/examples/git.ru +13 -0
- data/ext/unicorn/http11/http11.c +9 -2
- data/lib/unicorn.rb +35 -17
- data/lib/unicorn/app/exec_cgi.rb +10 -7
- data/lib/unicorn/app/inetd.rb +108 -0
- data/lib/unicorn/chunked_reader.rb +94 -0
- data/lib/unicorn/configurator.rb +1 -1
- data/lib/unicorn/const.rb +5 -1
- data/lib/unicorn/http_request.rb +16 -60
- data/lib/unicorn/http_response.rb +2 -3
- data/lib/unicorn/tee_input.rb +135 -0
- data/lib/unicorn/trailer_parser.rb +52 -0
- data/lib/unicorn/util.rb +0 -17
- data/local.mk.sample +3 -3
- data/test/rails/test_rails.rb +18 -12
- data/test/test_helper.rb +26 -0
- data/test/unit/test_chunked_reader.rb +180 -0
- data/test/unit/test_configurator.rb +1 -1
- data/test/unit/test_http_parser.rb +30 -0
- data/test/unit/test_request.rb +6 -1
- data/test/unit/test_server.rb +12 -1
- data/test/unit/test_signals.rb +2 -0
- data/test/unit/test_trailer_parser.rb +52 -0
- data/test/unit/test_upload.rb +130 -104
- data/test/unit/test_util.rb +28 -30
- data/unicorn.gemspec +7 -6
- metadata +19 -3
data/lib/unicorn/configurator.rb
CHANGED
@@ -65,7 +65,7 @@ module Unicorn
|
|
65
65
|
def commit!(server, options = {}) #:nodoc:
|
66
66
|
skip = options[:skip] || []
|
67
67
|
@set.each do |key, value|
|
68
|
-
|
68
|
+
value == :unset and next
|
69
69
|
skip.include?(key) and next
|
70
70
|
setter = "#{key}="
|
71
71
|
if server.respond_to?(setter)
|
data/lib/unicorn/const.rb
CHANGED
@@ -5,7 +5,7 @@ module Unicorn
|
|
5
5
|
# gave about a 3% to 10% performance improvement over using the strings directly.
|
6
6
|
# Symbols did not really improve things much compared to constants.
|
7
7
|
module Const
|
8
|
-
UNICORN_VERSION="0.
|
8
|
+
UNICORN_VERSION="0.9.0".freeze
|
9
9
|
|
10
10
|
DEFAULT_HOST = "0.0.0.0".freeze # default TCP listen host address
|
11
11
|
DEFAULT_PORT = "8080".freeze # default TCP listen port
|
@@ -24,11 +24,15 @@ module Unicorn
|
|
24
24
|
# common errors we'll send back
|
25
25
|
ERROR_400_RESPONSE = "HTTP/1.1 400 Bad Request\r\n\r\n".freeze
|
26
26
|
ERROR_500_RESPONSE = "HTTP/1.1 500 Internal Server Error\r\n\r\n".freeze
|
27
|
+
EXPECT_100_RESPONSE = "HTTP/1.1 100 Continue\r\n\r\n"
|
27
28
|
|
28
29
|
# A frozen format for this is about 15% faster
|
30
|
+
HTTP_TRANSFER_ENCODING = 'HTTP_TRANSFER_ENCODING'.freeze
|
29
31
|
CONTENT_LENGTH="CONTENT_LENGTH".freeze
|
30
32
|
REMOTE_ADDR="REMOTE_ADDR".freeze
|
31
33
|
HTTP_X_FORWARDED_FOR="HTTP_X_FORWARDED_FOR".freeze
|
34
|
+
HTTP_EXPECT="HTTP_EXPECT".freeze
|
35
|
+
HTTP_TRAILER="HTTP_TRAILER".freeze
|
32
36
|
RACK_INPUT="rack.input".freeze
|
33
37
|
end
|
34
38
|
|
data/lib/unicorn/http_request.rb
CHANGED
@@ -1,15 +1,9 @@
|
|
1
|
-
require 'tempfile'
|
2
1
|
require 'stringio'
|
3
2
|
|
4
3
|
# compiled extension
|
5
4
|
require 'unicorn/http11'
|
6
5
|
|
7
6
|
module Unicorn
|
8
|
-
#
|
9
|
-
# The HttpRequest.initialize method will convert any request that is larger than
|
10
|
-
# Const::MAX_BODY into a Tempfile and use that as the body. Otherwise it uses
|
11
|
-
# a StringIO object. To be safe, you should assume it works like a file.
|
12
|
-
#
|
13
7
|
class HttpRequest
|
14
8
|
|
15
9
|
attr_accessor :logger
|
@@ -27,14 +21,13 @@ module Unicorn
|
|
27
21
|
"SERVER_SOFTWARE" => "Unicorn #{Const::UNICORN_VERSION}".freeze
|
28
22
|
}
|
29
23
|
|
30
|
-
|
31
|
-
# (GET/HEAD) requests.
|
32
|
-
NULL_IO = StringIO.new
|
24
|
+
NULL_IO = StringIO.new(Z)
|
33
25
|
LOCALHOST = '127.0.0.1'.freeze
|
34
26
|
|
35
27
|
# Being explicitly single-threaded, we have certain advantages in
|
36
28
|
# not having to worry about variables being clobbered :)
|
37
29
|
BUFFER = ' ' * Const::CHUNK_SIZE # initial size, may grow
|
30
|
+
BUFFER.force_encoding(Encoding::BINARY) if Z.respond_to?(:force_encoding)
|
38
31
|
PARSER = HttpParser.new
|
39
32
|
PARAMS = Hash.new
|
40
33
|
|
@@ -56,11 +49,6 @@ module Unicorn
|
|
56
49
|
# This does minimal exception trapping and it is up to the caller
|
57
50
|
# to handle any socket errors (e.g. user aborted upload).
|
58
51
|
def read(socket)
|
59
|
-
# reset the parser
|
60
|
-
unless NULL_IO == (input = PARAMS[Const::RACK_INPUT]) # unlikely
|
61
|
-
input.close rescue nil
|
62
|
-
input.close! rescue nil
|
63
|
-
end
|
64
52
|
PARAMS.clear
|
65
53
|
PARSER.reset
|
66
54
|
|
@@ -98,57 +86,25 @@ module Unicorn
|
|
98
86
|
private
|
99
87
|
|
100
88
|
# Handles dealing with the rest of the request
|
101
|
-
# returns a Rack environment if successful
|
89
|
+
# returns a Rack environment if successful
|
102
90
|
def handle_body(socket)
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
91
|
+
PARAMS[Const::RACK_INPUT] = if (body = PARAMS.delete(:http_body))
|
92
|
+
length = PARAMS[Const::CONTENT_LENGTH].to_i
|
93
|
+
|
94
|
+
if te = PARAMS[Const::HTTP_TRANSFER_ENCODING]
|
95
|
+
if /\Achunked\z/i =~ te
|
96
|
+
socket = ChunkedReader.new(PARAMS, socket, body)
|
97
|
+
length = body = nil
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
TeeInput.new(socket, length, body)
|
102
|
+
else
|
103
|
+
NULL_IO.closed? ? NULL_IO.reopen(Z) : NULL_IO
|
109
104
|
end
|
110
105
|
|
111
|
-
# must read more data to complete body
|
112
|
-
remain = content_length - http_body.length
|
113
|
-
|
114
|
-
body = PARAMS[Const::RACK_INPUT] = (remain < Const::MAX_BODY) ?
|
115
|
-
StringIO.new : Tempfile.new('unicorn')
|
116
|
-
|
117
|
-
body.binmode
|
118
|
-
body.write(http_body)
|
119
|
-
|
120
|
-
# Some clients (like FF1.0) report 0 for body and then send a body.
|
121
|
-
# This will probably truncate them but at least the request goes through
|
122
|
-
# usually.
|
123
|
-
read_body(socket, remain, body) if remain > 0
|
124
|
-
body.rewind
|
125
|
-
|
126
|
-
# in case read_body overread because the client tried to pipeline
|
127
|
-
# another request, we'll truncate it. Again, we don't do pipelining
|
128
|
-
# or keepalive
|
129
|
-
body.truncate(content_length)
|
130
106
|
PARAMS.update(DEFAULTS)
|
131
107
|
end
|
132
108
|
|
133
|
-
# Does the heavy lifting of properly reading the larger body
|
134
|
-
# requests in small chunks. It expects PARAMS['rack.input'] to be
|
135
|
-
# an IO object, socket to be valid, It also expects any initial part
|
136
|
-
# of the body that has been read to be in the PARAMS['rack.input']
|
137
|
-
# already. It will return true if successful and false if not.
|
138
|
-
def read_body(socket, remain, body)
|
139
|
-
begin
|
140
|
-
# write always writes the requested amount on a POSIX filesystem
|
141
|
-
remain -= body.write(socket.readpartial(Const::CHUNK_SIZE, BUFFER))
|
142
|
-
end while remain > 0
|
143
|
-
rescue Object => e
|
144
|
-
@logger.error "Error reading HTTP body: #{e.inspect}"
|
145
|
-
|
146
|
-
# Any errors means we should delete the file, including if the file
|
147
|
-
# is dumped. Truncate it ASAP to help avoid page flushes to disk.
|
148
|
-
body.truncate(0) rescue nil
|
149
|
-
reset
|
150
|
-
raise e
|
151
|
-
end
|
152
|
-
|
153
109
|
end
|
154
110
|
end
|
@@ -31,13 +31,12 @@ module Unicorn
|
|
31
31
|
# Connection: and Date: headers no matter what (if anything) our
|
32
32
|
# Rack application sent us.
|
33
33
|
SKIP = { 'connection' => true, 'date' => true, 'status' => true }.freeze
|
34
|
-
EMPTY = ''.freeze # :nodoc
|
35
34
|
OUT = [] # :nodoc
|
36
35
|
|
37
36
|
# writes the rack_response to socket as an HTTP response
|
38
37
|
def self.write(socket, rack_response)
|
39
38
|
status, headers, body = rack_response
|
40
|
-
status = CODES[status.to_i]
|
39
|
+
status = CODES[status.to_i]
|
41
40
|
OUT.clear
|
42
41
|
|
43
42
|
# Don't bother enforcing duplicate supression, it's a Hash most of
|
@@ -59,7 +58,7 @@ module Unicorn
|
|
59
58
|
"Date: #{Time.now.httpdate}\r\n" \
|
60
59
|
"Status: #{status}\r\n" \
|
61
60
|
"Connection: close\r\n" \
|
62
|
-
"#{OUT.join(
|
61
|
+
"#{OUT.join(Z)}\r\n")
|
63
62
|
body.each { |chunk| socket.write(chunk) }
|
64
63
|
socket.close # flushes and uncorks the socket immediately
|
65
64
|
ensure
|
@@ -0,0 +1,135 @@
|
|
1
|
+
# Copyright (c) 2009 Eric Wong
|
2
|
+
# You can redistribute it and/or modify it under the same terms as Ruby.
|
3
|
+
|
4
|
+
require 'tempfile'
|
5
|
+
|
6
|
+
# acts like tee(1) on an input input to provide a input-like stream
|
7
|
+
# while providing rewindable semantics through a Tempfile/StringIO
|
8
|
+
# backing store. On the first pass, the input is only read on demand
|
9
|
+
# so your Rack application can use input notification (upload progress
|
10
|
+
# and like). This should fully conform to the Rack::InputWrapper
|
11
|
+
# specification on the public API. This class is intended to be a
|
12
|
+
# strict interpretation of Rack::InputWrapper functionality and will
|
13
|
+
# not support any deviations from it.
|
14
|
+
|
15
|
+
module Unicorn
|
16
|
+
class TeeInput
|
17
|
+
|
18
|
+
def initialize(input, size, body)
|
19
|
+
@tmp = Tempfile.new(nil)
|
20
|
+
@tmp.unlink
|
21
|
+
@tmp.binmode
|
22
|
+
@tmp.sync = true
|
23
|
+
|
24
|
+
if body
|
25
|
+
@tmp.write(body)
|
26
|
+
@tmp.seek(0)
|
27
|
+
end
|
28
|
+
@input = input
|
29
|
+
@size = size # nil if chunked
|
30
|
+
end
|
31
|
+
|
32
|
+
# returns the size of the input. This is what the Content-Length
|
33
|
+
# header value should be, and how large our input is expected to be.
|
34
|
+
# For TE:chunked, this requires consuming all of the input stream
|
35
|
+
# before returning since there's no other way
|
36
|
+
def size
|
37
|
+
@size and return @size
|
38
|
+
|
39
|
+
if @input
|
40
|
+
buf = Z.dup
|
41
|
+
while tee(Const::CHUNK_SIZE, buf)
|
42
|
+
end
|
43
|
+
@tmp.rewind
|
44
|
+
end
|
45
|
+
|
46
|
+
@size = @tmp.stat.size
|
47
|
+
end
|
48
|
+
|
49
|
+
def read(*args)
|
50
|
+
@input or return @tmp.read(*args)
|
51
|
+
|
52
|
+
length = args.shift
|
53
|
+
if nil == length
|
54
|
+
rv = @tmp.read || Z.dup
|
55
|
+
tmp = Z.dup
|
56
|
+
while tee(Const::CHUNK_SIZE, tmp)
|
57
|
+
rv << tmp
|
58
|
+
end
|
59
|
+
rv
|
60
|
+
else
|
61
|
+
buf = args.shift || Z.dup
|
62
|
+
diff = @tmp.stat.size - @tmp.pos
|
63
|
+
if 0 == diff
|
64
|
+
tee(length, buf)
|
65
|
+
else
|
66
|
+
@tmp.read(diff > length ? length : diff, buf)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# takes zero arguments for strict Rack::Lint compatibility, unlike IO#gets
|
72
|
+
def gets
|
73
|
+
@input or return @tmp.gets
|
74
|
+
nil == $/ and return read
|
75
|
+
|
76
|
+
line = nil
|
77
|
+
if @tmp.pos < @tmp.stat.size
|
78
|
+
line = @tmp.gets # cannot be nil here
|
79
|
+
$/ == line[-$/.size, $/.size] and return line
|
80
|
+
|
81
|
+
# half the line was already read, and the rest of has not been read
|
82
|
+
if buf = @input.gets
|
83
|
+
@tmp.write(buf)
|
84
|
+
line << buf
|
85
|
+
else
|
86
|
+
@input = nil
|
87
|
+
end
|
88
|
+
elsif line = @input.gets
|
89
|
+
@tmp.write(line)
|
90
|
+
end
|
91
|
+
|
92
|
+
line
|
93
|
+
end
|
94
|
+
|
95
|
+
def each(&block)
|
96
|
+
while line = gets
|
97
|
+
yield line
|
98
|
+
end
|
99
|
+
|
100
|
+
self # Rack does not specify what the return value here
|
101
|
+
end
|
102
|
+
|
103
|
+
def rewind
|
104
|
+
@tmp.rewind # Rack does not specify what the return value here
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
# tees off a +length+ chunk of data from the input into the IO
|
110
|
+
# backing store as well as returning it. +buf+ must be specified.
|
111
|
+
# returns nil if reading from the input returns nil
|
112
|
+
def tee(length, buf)
|
113
|
+
begin
|
114
|
+
if @size
|
115
|
+
left = @size - @tmp.stat.size
|
116
|
+
0 == left and return nil
|
117
|
+
if length >= left
|
118
|
+
@input.readpartial(left, buf) == left and @input = nil
|
119
|
+
elsif @input.nil?
|
120
|
+
return nil
|
121
|
+
else
|
122
|
+
@input.readpartial(length, buf)
|
123
|
+
end
|
124
|
+
else # ChunkedReader#readpartial just raises EOFError when done
|
125
|
+
@input.readpartial(length, buf)
|
126
|
+
end
|
127
|
+
rescue EOFError
|
128
|
+
return @input = nil
|
129
|
+
end
|
130
|
+
@tmp.write(buf)
|
131
|
+
buf
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# Copyright (c) 2009 Eric Wong
|
2
|
+
# You can redistribute it and/or modify it under the same terms as Ruby.
|
3
|
+
require 'unicorn'
|
4
|
+
require 'unicorn/http11'
|
5
|
+
|
6
|
+
# Eventually I should integrate this into HttpParser...
|
7
|
+
module Unicorn
|
8
|
+
class TrailerParser
|
9
|
+
|
10
|
+
TR_FR = 'a-z-'.freeze
|
11
|
+
TR_TO = 'A-Z_'.freeze
|
12
|
+
|
13
|
+
# initializes HTTP trailer parser with acceptable +trailer+
|
14
|
+
def initialize(http_trailer)
|
15
|
+
@trailers = http_trailer.split(/\s*,\s*/).inject({}) { |hash, key|
|
16
|
+
hash[key.tr(TR_FR, TR_TO)] = true
|
17
|
+
hash
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
# Executes our TrailerParser on +data+ and modifies +env+ This will
|
22
|
+
# shrink +data+ as it is being consumed. Returns true if it has
|
23
|
+
# parsed all trailers, false if not. It raises HttpParserError on
|
24
|
+
# parse failure or unknown headers. It has slightly smaller limits
|
25
|
+
# than the C-based HTTP parser but should not be an issue in practice
|
26
|
+
# since Content-MD5 is probably the only legitimate use for it.
|
27
|
+
def execute!(env, data)
|
28
|
+
data.size > 0xffff and
|
29
|
+
raise HttpParserError, "trailer buffer too large: #{data.size} bytes"
|
30
|
+
|
31
|
+
begin
|
32
|
+
data.sub!(/\A([^\r]+)\r\n/, Z) or return false # need more data
|
33
|
+
|
34
|
+
key, val = $1.split(/:\s*/, 2)
|
35
|
+
|
36
|
+
key.size > 256 and
|
37
|
+
raise HttpParserError, "trailer key #{key.inspect} is too long"
|
38
|
+
val.size > 8192 and
|
39
|
+
raise HttpParserError, "trailer value #{val.inspect} is too long"
|
40
|
+
|
41
|
+
key.tr!(TR_FR, TR_TO)
|
42
|
+
|
43
|
+
@trailers.delete(key) or
|
44
|
+
raise HttpParserError, "unknown trailer: #{key.inspect}"
|
45
|
+
env["HTTP_#{key}"] = val
|
46
|
+
|
47
|
+
@trailers.empty? and return true
|
48
|
+
end while true
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
data/lib/unicorn/util.rb
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
require 'fcntl'
|
2
|
-
require 'tmpdir'
|
3
2
|
|
4
3
|
module Unicorn
|
5
4
|
class Util
|
@@ -40,22 +39,6 @@ module Unicorn
|
|
40
39
|
nr
|
41
40
|
end
|
42
41
|
|
43
|
-
# creates and returns a new File object. The File is unlinked
|
44
|
-
# immediately, switched to binary mode, and userspace output
|
45
|
-
# buffering is disabled
|
46
|
-
def tmpio
|
47
|
-
fp = begin
|
48
|
-
File.open("#{Dir::tmpdir}/#{rand}",
|
49
|
-
File::RDWR|File::CREAT|File::EXCL, 0600)
|
50
|
-
rescue Errno::EEXIST
|
51
|
-
retry
|
52
|
-
end
|
53
|
-
File.unlink(fp.path)
|
54
|
-
fp.binmode
|
55
|
-
fp.sync = true
|
56
|
-
fp
|
57
|
-
end
|
58
|
-
|
59
42
|
end
|
60
43
|
|
61
44
|
end
|
data/local.mk.sample
CHANGED
@@ -38,7 +38,7 @@ publish_doc:
|
|
38
38
|
# Create gzip variants of the same timestamp as the original so nginx
|
39
39
|
# "gzip_static on" can serve the gzipped versions directly.
|
40
40
|
doc_gz: suf := html js css
|
41
|
-
doc_gz:
|
42
|
-
doc_gz: docs := $(wildcard $(globs))
|
41
|
+
doc_gz: docs = $(shell find doc/ -regex '^.*\.\(html\|js\|css\)$$')
|
43
42
|
doc_gz:
|
44
|
-
for i in $(docs); do
|
43
|
+
for i in $(docs); do \
|
44
|
+
gzip --rsyncable < $$i > $$i.gz; touch -r $$i $$i.gz; done
|
data/test/rails/test_rails.rb
CHANGED
@@ -142,18 +142,24 @@ logger Logger.new('#{COMMON_TMP.path}')
|
|
142
142
|
end
|
143
143
|
end
|
144
144
|
end
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
145
|
+
|
146
|
+
# fixed in Rack commit 44ed4640f077504a49b7f1cabf8d6ad7a13f6441,
|
147
|
+
# no released version of Rails or Rack has this fix
|
148
|
+
if RB_V[0] >= 1 && RB_V[1] >= 9
|
149
|
+
warn "multipart broken with Rack 1.0.0 and Rails 2.3.2.1 under 1.9"
|
150
|
+
else
|
151
|
+
resp = `curl -isSfN -Ffile=@#{tmp.path} http://#@addr:#@port/foo/xpost`
|
152
|
+
assert $?.success?
|
153
|
+
resp = resp.split(/\r?\n/)
|
154
|
+
grepped = resp.grep(/^sha1: (.{40})/)
|
155
|
+
assert_equal 1, grepped.size
|
156
|
+
assert_equal(sha1.hexdigest, /^sha1: (.{40})/.match(grepped.first)[1])
|
157
|
+
|
158
|
+
grepped = resp.grep(/^Content-Type:\s+(.+)/i)
|
159
|
+
assert_equal 1, grepped.size
|
160
|
+
assert_match %r{^text/plain}, grepped.first.split(/\s*:\s*/)[1]
|
161
|
+
assert_equal 1, resp.grep(/^Status:/i).size
|
162
|
+
end
|
157
163
|
|
158
164
|
# make sure we can get 403 responses, too
|
159
165
|
uri = URI.parse("http://#@addr:#@port/foo/xpost")
|
data/test/test_helper.rb
CHANGED
@@ -262,3 +262,29 @@ def wait_for_death(pid)
|
|
262
262
|
end
|
263
263
|
raise "PID:#{pid} never died!"
|
264
264
|
end
|
265
|
+
|
266
|
+
# executes +cmd+ and chunks its STDOUT
|
267
|
+
def chunked_spawn(stdout, *cmd)
|
268
|
+
fork {
|
269
|
+
crd, cwr = IO.pipe
|
270
|
+
crd.binmode
|
271
|
+
cwr.binmode
|
272
|
+
crd.sync = cwr.sync = true
|
273
|
+
|
274
|
+
pid = fork {
|
275
|
+
STDOUT.reopen(cwr)
|
276
|
+
crd.close
|
277
|
+
cwr.close
|
278
|
+
exec(*cmd)
|
279
|
+
}
|
280
|
+
cwr.close
|
281
|
+
begin
|
282
|
+
buf = crd.readpartial(16384)
|
283
|
+
stdout.write("#{'%x' % buf.size}\r\n#{buf}")
|
284
|
+
rescue EOFError
|
285
|
+
stdout.write("0\r\n")
|
286
|
+
pid, status = Process.waitpid(pid)
|
287
|
+
exit status.exitstatus
|
288
|
+
end while true
|
289
|
+
}
|
290
|
+
end
|