thin 0.5.4-x86-mswin32-60 → 0.6.0-x86-mswin32-60
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of thin might be problematic. Click here for more details.
- data/CHANGELOG +40 -3
- data/README +13 -5
- data/Rakefile +2 -2
- data/bin/thin +46 -9
- data/example/adapter.rb +17 -0
- data/example/config.ru +10 -3
- data/ext/thin_parser/parser.c +122 -80
- data/ext/thin_parser/parser.rl +25 -9
- data/lib/thin.rb +1 -0
- data/lib/thin/cluster.rb +53 -26
- data/lib/thin/connection.rb +20 -3
- data/lib/thin/daemonizing.rb +1 -1
- data/lib/thin/headers.rb +7 -20
- data/lib/thin/logging.rb +1 -1
- data/lib/thin/request.rb +50 -21
- data/lib/thin/response.rb +4 -2
- data/lib/thin/server.rb +77 -36
- data/lib/thin/stats.rb +64 -0
- data/lib/thin/version.rb +4 -4
- data/lib/thin_parser.so +0 -0
- data/spec/cluster_spec.rb +65 -15
- data/spec/daemonizing_spec.rb +2 -1
- data/spec/headers_spec.rb +2 -8
- data/spec/request_spec.rb +24 -3
- data/spec/response_spec.rb +1 -1
- data/spec/server_spec.rb +95 -14
- data/spec/spec_helper.rb +25 -5
- data/tasks/announce.rake +18 -0
- data/tasks/deploy.rake +16 -0
- data/tasks/email.erb +46 -0
- data/tasks/ext.rake +38 -0
- data/tasks/gem.rake +76 -0
- data/tasks/rdoc.rake +25 -0
- data/tasks/site.rake +15 -0
- data/tasks/spec.rake +26 -0
- data/tasks/stats.rake +15 -0
- metadata +14 -3
data/ext/thin_parser/parser.rl
CHANGED
@@ -29,34 +29,50 @@
|
|
29
29
|
|
30
30
|
action start_value { MARK(mark, fpc); }
|
31
31
|
action write_value {
|
32
|
-
parser->http_field
|
32
|
+
if (parser->http_field != NULL) {
|
33
|
+
parser->http_field(parser->data, PTR_TO(field_start), parser->field_len, PTR_TO(mark), LEN(mark, fpc));
|
34
|
+
}
|
33
35
|
}
|
34
36
|
action request_method {
|
35
|
-
parser->request_method
|
37
|
+
if (parser->request_method != NULL) {
|
38
|
+
parser->request_method(parser->data, PTR_TO(mark), LEN(mark, fpc));
|
39
|
+
}
|
36
40
|
}
|
37
|
-
action request_uri {
|
38
|
-
parser->request_uri
|
41
|
+
action request_uri {
|
42
|
+
if (parser->request_uri != NULL) {
|
43
|
+
parser->request_uri(parser->data, PTR_TO(mark), LEN(mark, fpc));
|
44
|
+
}
|
39
45
|
}
|
40
46
|
action fragment {
|
41
|
-
parser->fragment
|
47
|
+
if (parser->fragment != NULL) {
|
48
|
+
parser->fragment(parser->data, PTR_TO(mark), LEN(mark, fpc));
|
49
|
+
}
|
42
50
|
}
|
43
51
|
|
44
52
|
action start_query {MARK(query_start, fpc); }
|
45
53
|
action query_string {
|
46
|
-
parser->query_string
|
54
|
+
if (parser->query_string != NULL) {
|
55
|
+
parser->query_string(parser->data, PTR_TO(query_start), LEN(query_start, fpc));
|
56
|
+
}
|
47
57
|
}
|
48
58
|
|
49
59
|
action http_version {
|
50
|
-
parser->http_version
|
60
|
+
if (parser->http_version != NULL) {
|
61
|
+
parser->http_version(parser->data, PTR_TO(mark), LEN(mark, fpc));
|
62
|
+
}
|
51
63
|
}
|
52
64
|
|
53
65
|
action request_path {
|
54
|
-
parser->request_path
|
66
|
+
if (parser->request_path != NULL) {
|
67
|
+
parser->request_path(parser->data, PTR_TO(mark), LEN(mark,fpc));
|
68
|
+
}
|
55
69
|
}
|
56
70
|
|
57
71
|
action done {
|
58
72
|
parser->body_start = fpc - buffer + 1;
|
59
|
-
parser->header_done
|
73
|
+
if (parser->header_done != NULL) {
|
74
|
+
parser->header_done(parser->data, fpc + 1, pe - fpc - 1);
|
75
|
+
}
|
60
76
|
fbreak;
|
61
77
|
}
|
62
78
|
|
data/lib/thin.rb
CHANGED
data/lib/thin/cluster.rb
CHANGED
@@ -20,36 +20,42 @@ module Thin
|
|
20
20
|
def initialize(options)
|
21
21
|
@options = options.merge(:daemonize => true)
|
22
22
|
@size = @options.delete(:servers)
|
23
|
-
@script = 'thin'
|
23
|
+
@script = File.join(File.dirname(__FILE__), '..', '..', 'bin', 'thin')
|
24
|
+
|
25
|
+
if socket
|
26
|
+
@options.delete(:address)
|
27
|
+
@options.delete(:port)
|
28
|
+
end
|
24
29
|
end
|
25
30
|
|
26
31
|
def first_port; @options[:port] end
|
27
|
-
def address; @options[:address] end
|
32
|
+
def address; @options[:address] end
|
33
|
+
def socket; @options[:socket] end
|
28
34
|
def pid_file; File.expand_path File.join(@options[:chdir], @options[:pid]) end
|
29
35
|
def log_file; File.expand_path File.join(@options[:chdir], @options[:log]) end
|
30
36
|
|
31
37
|
# Start the servers
|
32
38
|
def start
|
33
|
-
with_each_server { |port|
|
39
|
+
with_each_server { |port| start_server port }
|
34
40
|
end
|
35
41
|
|
36
|
-
# Start
|
37
|
-
def
|
38
|
-
log "Starting #{
|
42
|
+
# Start a single server
|
43
|
+
def start_server(number)
|
44
|
+
log "Starting server on #{server_id(number)} ... "
|
39
45
|
|
40
|
-
run :start, @options,
|
46
|
+
run :start, @options, number
|
41
47
|
end
|
42
48
|
|
43
49
|
# Stop the servers
|
44
50
|
def stop
|
45
|
-
with_each_server { |
|
51
|
+
with_each_server { |n| stop_server n }
|
46
52
|
end
|
47
53
|
|
48
|
-
# Stop
|
49
|
-
def
|
50
|
-
log "Stopping #{
|
54
|
+
# Stop a single server
|
55
|
+
def stop_server(number)
|
56
|
+
log "Stopping server on #{server_id(number)} ... "
|
51
57
|
|
52
|
-
run :stop, @options,
|
58
|
+
run :stop, @options, number
|
53
59
|
end
|
54
60
|
|
55
61
|
# Stop and start the servers.
|
@@ -59,25 +65,44 @@ module Thin
|
|
59
65
|
start
|
60
66
|
end
|
61
67
|
|
62
|
-
def
|
63
|
-
|
68
|
+
def server_id(number)
|
69
|
+
if socket
|
70
|
+
socket_for(number)
|
71
|
+
else
|
72
|
+
[address, number].join(':')
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def log_file_for(number)
|
77
|
+
include_server_number log_file, number
|
64
78
|
end
|
65
79
|
|
66
|
-
def pid_file_for(
|
67
|
-
|
80
|
+
def pid_file_for(number)
|
81
|
+
include_server_number pid_file, number
|
68
82
|
end
|
69
83
|
|
70
|
-
def
|
71
|
-
|
84
|
+
def socket_for(number)
|
85
|
+
include_server_number socket, number
|
86
|
+
end
|
87
|
+
|
88
|
+
def pid_for(number)
|
89
|
+
File.read(pid_file_for(number)).chomp.to_i
|
72
90
|
end
|
73
91
|
|
74
92
|
private
|
75
93
|
# Send the command to the +thin+ script
|
76
|
-
def run(cmd, options,
|
77
|
-
|
94
|
+
def run(cmd, options, number)
|
95
|
+
cmd_options = options.dup
|
96
|
+
cmd_options.merge!(:pid => pid_file_for(number), :log => log_file_for(number))
|
97
|
+
if socket
|
98
|
+
cmd_options.merge!(:socket => socket_for(number))
|
99
|
+
else
|
100
|
+
cmd_options.merge!(:port => number)
|
101
|
+
end
|
102
|
+
shell_cmd = shellify(cmd, cmd_options)
|
78
103
|
trace shell_cmd
|
79
104
|
ouput = `#{shell_cmd}`.chomp
|
80
|
-
log ouput unless ouput.empty?
|
105
|
+
log " " + ouput.gsub("\n", " \n") unless ouput.empty?
|
81
106
|
end
|
82
107
|
|
83
108
|
# Turn into a runnable shell command
|
@@ -93,14 +118,16 @@ module Thin
|
|
93
118
|
end
|
94
119
|
|
95
120
|
def with_each_server
|
96
|
-
@size.times
|
121
|
+
@size.times do |n|
|
122
|
+
yield socket ? n : (first_port + n)
|
123
|
+
end
|
97
124
|
end
|
98
125
|
|
99
|
-
# Add the port
|
126
|
+
# Add the server port or number in the filename
|
100
127
|
# so each instance get its own file
|
101
|
-
def
|
102
|
-
|
103
|
-
path.gsub(
|
128
|
+
def include_server_number(path, number)
|
129
|
+
ext = File.extname(path)
|
130
|
+
path.gsub(/#{ext}$/, ".#{number}#{ext}")
|
104
131
|
end
|
105
132
|
end
|
106
133
|
end
|
data/lib/thin/connection.rb
CHANGED
@@ -4,8 +4,12 @@ module Thin
|
|
4
4
|
class Connection < EventMachine::Connection
|
5
5
|
include Logging
|
6
6
|
|
7
|
+
# Rack application served by this connection.
|
7
8
|
attr_accessor :app
|
8
9
|
|
10
|
+
# +true+ if the connection is on a UNIX domain socket.
|
11
|
+
attr_accessor :unix_socket
|
12
|
+
|
9
13
|
def post_init
|
10
14
|
@request = Request.new
|
11
15
|
@response = Response.new
|
@@ -13,7 +17,7 @@ module Thin
|
|
13
17
|
|
14
18
|
def receive_data(data)
|
15
19
|
trace { data }
|
16
|
-
|
20
|
+
process if @request.parse(data)
|
17
21
|
rescue InvalidRequest => e
|
18
22
|
log "Invalid request"
|
19
23
|
log_error e
|
@@ -24,8 +28,8 @@ module Thin
|
|
24
28
|
env = @request.env
|
25
29
|
|
26
30
|
# Add client info to the request env
|
27
|
-
env[Request::REMOTE_ADDR] = env
|
28
|
-
|
31
|
+
env[Request::REMOTE_ADDR] = remote_address(env)
|
32
|
+
|
29
33
|
# Process the request
|
30
34
|
@response.status, @response.headers, @response.body = @app.call(env)
|
31
35
|
|
@@ -42,7 +46,20 @@ module Thin
|
|
42
46
|
log_error e
|
43
47
|
close_connection rescue nil
|
44
48
|
ensure
|
49
|
+
@request.close rescue nil
|
45
50
|
@response.close rescue nil
|
46
51
|
end
|
52
|
+
|
53
|
+
protected
|
54
|
+
def remote_address(env)
|
55
|
+
if remote_addr = env[Request::FORWARDED_FOR]
|
56
|
+
remote_addr
|
57
|
+
elsif @unix_socket
|
58
|
+
# FIXME not sure about this, does it even make sense on a UNIX socket?
|
59
|
+
Socket.unpack_sockaddr_un(get_peername)
|
60
|
+
else
|
61
|
+
Socket.unpack_sockaddr_in(get_peername)[1]
|
62
|
+
end
|
63
|
+
end
|
47
64
|
end
|
48
65
|
end
|
data/lib/thin/daemonizing.rb
CHANGED
data/lib/thin/headers.rb
CHANGED
@@ -1,37 +1,24 @@
|
|
1
1
|
module Thin
|
2
|
-
#
|
2
|
+
# Store HTTP header name-value pairs direcly to a string
|
3
|
+
# and allow duplicated entries on some names.
|
3
4
|
class Headers
|
4
5
|
HEADER_FORMAT = "%s: %s\r\n".freeze
|
5
6
|
ALLOWED_DUPLICATES = %w(Set-Cookie Set-Cookie2 Warning WWW-Authenticate).freeze
|
6
7
|
|
7
8
|
def initialize
|
8
9
|
@sent = {}
|
9
|
-
@
|
10
|
+
@out = ''
|
10
11
|
end
|
11
|
-
|
12
|
+
|
12
13
|
def []=(key, value)
|
13
|
-
if
|
14
|
-
# If we don't allow duplicate for that field
|
15
|
-
# we overwrite the one that is already there
|
16
|
-
@items.assoc(key)[1] = value
|
17
|
-
else
|
14
|
+
if !@sent[key] || ALLOWED_DUPLICATES.include?(key)
|
18
15
|
@sent[key] = true
|
19
|
-
@
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
def [](key)
|
24
|
-
if item = @items.assoc(key)
|
25
|
-
item[1]
|
16
|
+
@out << HEADER_FORMAT % [key, value]
|
26
17
|
end
|
27
18
|
end
|
28
19
|
|
29
|
-
def size
|
30
|
-
@items.size
|
31
|
-
end
|
32
|
-
|
33
20
|
def to_s
|
34
|
-
@
|
21
|
+
@out
|
35
22
|
end
|
36
23
|
end
|
37
24
|
end
|
data/lib/thin/logging.rb
CHANGED
data/lib/thin/request.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'thin_parser'
|
2
|
+
require 'tempfile'
|
2
3
|
|
3
4
|
module Thin
|
4
5
|
# Raised when an incoming request is not valid
|
@@ -7,15 +8,18 @@ module Thin
|
|
7
8
|
|
8
9
|
# A request sent by the client to the server.
|
9
10
|
class Request
|
10
|
-
|
11
|
-
|
12
|
-
|
11
|
+
# Maximum request body size before it is moved out of memory
|
12
|
+
# and into a tempfile for reading.
|
13
|
+
MAX_BODY = 1024 * (80 + 32)
|
14
|
+
BODY_TMPFILE = 'thin-body'.freeze
|
15
|
+
|
16
|
+
# Freeze some HTTP header names
|
13
17
|
SERVER_SOFTWARE = 'SERVER_SOFTWARE'.freeze
|
14
18
|
REMOTE_ADDR = 'REMOTE_ADDR'.freeze
|
15
19
|
FORWARDED_FOR = 'HTTP_X_FORWARDED_FOR'.freeze
|
16
|
-
|
17
20
|
CONTENT_LENGTH = 'CONTENT_LENGTH'.freeze
|
18
|
-
|
21
|
+
|
22
|
+
# Freeze some Rack header names
|
19
23
|
RACK_INPUT = 'rack.input'.freeze
|
20
24
|
RACK_VERSION = 'rack.version'.freeze
|
21
25
|
RACK_ERRORS = 'rack.errors'.freeze
|
@@ -23,7 +27,14 @@ module Thin
|
|
23
27
|
RACK_MULTIPROCESS = 'rack.multiprocess'.freeze
|
24
28
|
RACK_RUN_ONCE = 'rack.run_once'.freeze
|
25
29
|
|
26
|
-
|
30
|
+
# CGI-like request environment variables
|
31
|
+
attr_reader :env
|
32
|
+
|
33
|
+
# Unparsed data of the request
|
34
|
+
attr_reader :data
|
35
|
+
|
36
|
+
# Request body
|
37
|
+
attr_reader :body
|
27
38
|
|
28
39
|
def initialize
|
29
40
|
@parser = HttpParser.new
|
@@ -45,28 +56,46 @@ module Thin
|
|
45
56
|
}
|
46
57
|
end
|
47
58
|
|
59
|
+
# Parse a chunk of data into the request environment
|
60
|
+
# Raises a +InvalidRequest+ if invalid.
|
61
|
+
# Returns +true+ if the parsing is complete.
|
48
62
|
def parse(data)
|
49
63
|
@data << data
|
50
|
-
|
51
|
-
|
64
|
+
|
65
|
+
if @parser.finished? # Header finished, can only be some more body
|
52
66
|
body << data
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
67
|
+
else # Parse more header using the super parser
|
68
|
+
@nparsed = @parser.execute(@env, @data, @nparsed)
|
69
|
+
# Transfert to a tempfile if body is very big
|
70
|
+
move_body_to_tempfile if @parser.finished? && content_length > MAX_BODY
|
71
|
+
end
|
72
|
+
|
73
|
+
# Check if header and body are complete
|
74
|
+
if @parser.finished? && @body.size >= content_length
|
75
|
+
@body.rewind
|
76
|
+
return true # Request is fully parsed
|
77
|
+
end
|
78
|
+
|
79
|
+
false # Not finished, need more data
|
66
80
|
end
|
67
81
|
|
82
|
+
# Expected size of the body
|
68
83
|
def content_length
|
69
84
|
@env[CONTENT_LENGTH].to_i
|
70
85
|
end
|
86
|
+
|
87
|
+
def close
|
88
|
+
@body.close if @body === Tempfile
|
89
|
+
end
|
90
|
+
|
91
|
+
|
92
|
+
private
|
93
|
+
def move_body_to_tempfile
|
94
|
+
current_body = @body
|
95
|
+
@body = Tempfile.new(BODY_TMPFILE)
|
96
|
+
@body.binmode
|
97
|
+
@body << current_body unless current_body.size.zero?
|
98
|
+
@env[RACK_INPUT] = @body
|
99
|
+
end
|
71
100
|
end
|
72
101
|
end
|
data/lib/thin/response.rb
CHANGED
@@ -36,7 +36,7 @@ module Thin
|
|
36
36
|
|
37
37
|
def headers=(key_value_pairs)
|
38
38
|
key_value_pairs.each do |k, vs|
|
39
|
-
vs.
|
39
|
+
vs.each_line { |v| @headers[k] = v.chomp }
|
40
40
|
end
|
41
41
|
end
|
42
42
|
|
@@ -45,7 +45,9 @@ module Thin
|
|
45
45
|
@body.close if @body.respond_to?(:close)
|
46
46
|
end
|
47
47
|
|
48
|
-
# Yields each chunk of the response
|
48
|
+
# Yields each chunk of the response.
|
49
|
+
# To control the size of each chunk
|
50
|
+
# define your own +each+ method on +body+.
|
49
51
|
def each
|
50
52
|
yield head
|
51
53
|
@body.each do |chunk|
|
data/lib/thin/server.rb
CHANGED
@@ -12,69 +12,110 @@ module Thin
|
|
12
12
|
include Logging
|
13
13
|
include Daemonizable
|
14
14
|
|
15
|
-
#
|
15
|
+
# Address and port on which the server is listening for connections.
|
16
16
|
attr_accessor :port, :host
|
17
17
|
|
18
|
-
#
|
18
|
+
# UNIX domain socket on which the server is listening for connections.
|
19
|
+
attr_accessor :socket
|
20
|
+
|
21
|
+
# App called with the request that produces the response.
|
19
22
|
attr_accessor :app
|
20
23
|
|
21
24
|
# Maximum time for incoming data to arrive
|
22
25
|
attr_accessor :timeout
|
23
26
|
|
24
|
-
# Creates a new server
|
25
|
-
# that will pass request to +app+.
|
26
|
-
|
27
|
-
|
28
|
-
|
27
|
+
# Creates a new server bound to <tt>host:port</tt>
|
28
|
+
# or to +socket+ that will pass request to +app+.
|
29
|
+
# If +host_or_socket+ contains a <tt>/</tt> it is assumed
|
30
|
+
# to be a UNIX domain socket filename.
|
31
|
+
# If a block is passed, a <tt>Rack::Builder</tt> instance
|
32
|
+
# will be passed to build the +app+.
|
33
|
+
#
|
34
|
+
# Server.new '0.0.0.0', 3000 do
|
35
|
+
# use Rack::CommonLogger
|
36
|
+
# use Rack::ShowExceptions
|
37
|
+
# map "/lobster" do
|
38
|
+
# use Rack::Lint
|
39
|
+
# run Rack::Lobster.new
|
40
|
+
# end
|
41
|
+
# end.start
|
42
|
+
#
|
43
|
+
def initialize(host_or_socket, port=3000, app=nil, &block)
|
44
|
+
if host_or_socket.include?('/')
|
45
|
+
@socket = host_or_socket
|
46
|
+
else
|
47
|
+
@host = host_or_socket
|
48
|
+
@port = port.to_i
|
49
|
+
end
|
29
50
|
@app = app
|
30
51
|
@timeout = 60 # sec
|
31
|
-
end
|
32
|
-
|
33
|
-
# Starts the handlers.
|
34
|
-
def start
|
35
|
-
raise ArgumentError, "app required" unless @app
|
36
52
|
|
37
|
-
|
38
|
-
trace ">> Tracing ON"
|
53
|
+
@app = Rack::Builder.new(&block).to_app if block
|
39
54
|
end
|
40
55
|
|
41
|
-
|
42
|
-
|
43
|
-
start
|
44
|
-
listen!
|
56
|
+
def self.start(*args, &block)
|
57
|
+
new(*args, &block).start!
|
45
58
|
end
|
46
59
|
|
47
|
-
# Start
|
48
|
-
def
|
49
|
-
|
50
|
-
trap('TERM') { stop! }
|
60
|
+
# Start the server and listen for connections
|
61
|
+
def start
|
62
|
+
raise ArgumentError, "app required" unless @app
|
51
63
|
|
64
|
+
trap('INT') { stop }
|
65
|
+
trap('TERM') { stop! }
|
66
|
+
|
52
67
|
# See http://rubyeventmachine.com/pub/rdoc/files/EPOLL.html
|
53
68
|
EventMachine.epoll
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
end
|
66
|
-
end
|
69
|
+
|
70
|
+
log ">> Thin web server (v#{VERSION::STRING} codename #{VERSION::CODENAME})"
|
71
|
+
trace ">> Tracing ON"
|
72
|
+
|
73
|
+
EventMachine.run do
|
74
|
+
begin
|
75
|
+
start_server
|
76
|
+
rescue StopServer
|
77
|
+
stop
|
78
|
+
end
|
79
|
+
end
|
67
80
|
end
|
81
|
+
alias :start! :start
|
68
82
|
|
69
|
-
|
83
|
+
# Stops the server by stopping the listening loop.
|
70
84
|
def stop
|
71
85
|
EventMachine.stop_event_loop
|
72
86
|
rescue
|
73
87
|
warn "Error stopping : #{$!}"
|
74
88
|
end
|
75
89
|
|
90
|
+
# Stops the server by raising an error.
|
76
91
|
def stop!
|
77
92
|
raise StopServer
|
78
93
|
end
|
94
|
+
|
95
|
+
protected
|
96
|
+
def start_server
|
97
|
+
if @socket
|
98
|
+
start_server_on_socket
|
99
|
+
else
|
100
|
+
start_server_on_host
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def start_server_on_host
|
105
|
+
log ">> Listening on #{@host}:#{@port}, CTRL+C to stop"
|
106
|
+
EventMachine.start_server(@host, @port, Connection, &method(:initialize_connection))
|
107
|
+
end
|
108
|
+
|
109
|
+
def start_server_on_socket
|
110
|
+
log ">> Listening on #{@socket}, CTRL+C to stop"
|
111
|
+
EventMachine.start_unix_domain_server(@socket, Connection, &method(:initialize_connection))
|
112
|
+
end
|
113
|
+
|
114
|
+
def initialize_connection(connection)
|
115
|
+
connection.comm_inactivity_timeout = @timeout
|
116
|
+
connection.app = @app
|
117
|
+
connection.silent = @silent
|
118
|
+
connection.unix_socket = !@socket.nil?
|
119
|
+
end
|
79
120
|
end
|
80
121
|
end
|