unicorn 0.8.4 → 0.9.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/.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
|