thin 0.3.0
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/README +35 -0
- data/Rakefile +101 -0
- data/bin/thin +48 -0
- data/bin/thin_cluster +53 -0
- data/doc/benchmarks.txt +271 -0
- data/lib/thin.rb +19 -0
- data/lib/thin/cgi.rb +159 -0
- data/lib/thin/cluster.rb +147 -0
- data/lib/thin/command.rb +49 -0
- data/lib/thin/commands/cluster/base.rb +24 -0
- data/lib/thin/commands/cluster/config.rb +34 -0
- data/lib/thin/commands/cluster/restart.rb +35 -0
- data/lib/thin/commands/cluster/start.rb +40 -0
- data/lib/thin/commands/cluster/stop.rb +28 -0
- data/lib/thin/commands/server/base.rb +7 -0
- data/lib/thin/commands/server/start.rb +33 -0
- data/lib/thin/commands/server/stop.rb +29 -0
- data/lib/thin/consts.rb +33 -0
- data/lib/thin/daemonizing.rb +122 -0
- data/lib/thin/handler.rb +57 -0
- data/lib/thin/headers.rb +36 -0
- data/lib/thin/logging.rb +30 -0
- data/lib/thin/mime_types.rb +619 -0
- data/lib/thin/rails.rb +44 -0
- data/lib/thin/recipes.rb +36 -0
- data/lib/thin/request.rb +132 -0
- data/lib/thin/response.rb +54 -0
- data/lib/thin/server.rb +141 -0
- data/lib/thin/statuses.rb +43 -0
- data/lib/thin/version.rb +9 -0
- data/lib/transat/parser.rb +247 -0
- metadata +82 -0
data/lib/thin.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__)
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
require 'timeout'
|
5
|
+
require 'stringio'
|
6
|
+
|
7
|
+
require 'thin/version'
|
8
|
+
require 'thin/consts'
|
9
|
+
require 'thin/statuses'
|
10
|
+
require 'thin/mime_types'
|
11
|
+
require 'thin/logging'
|
12
|
+
require 'thin/daemonizing'
|
13
|
+
require 'thin/server'
|
14
|
+
require 'thin/request'
|
15
|
+
require 'thin/headers'
|
16
|
+
require 'thin/response'
|
17
|
+
require 'thin/handler'
|
18
|
+
require 'thin/cgi'
|
19
|
+
require 'thin/rails'
|
data/lib/thin/cgi.rb
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
|
3
|
+
module Thin
|
4
|
+
# Class largely based on Mongrel::CGIWrapper
|
5
|
+
# http://mongrel.rubyforge.org by Zed A. Shaw <zedshaw at zedshaw dot com>
|
6
|
+
class CGIWrapper < ::CGI
|
7
|
+
public :env_table
|
8
|
+
attr_reader :options
|
9
|
+
# Set this to false if you want calls to CGIWrapper.out to not actually send
|
10
|
+
# the response until you force it.
|
11
|
+
attr_accessor :default_really_final
|
12
|
+
|
13
|
+
# these are stripped out of any keys passed to CGIWrapper.header function
|
14
|
+
REMOVED_KEYS = [ "nph","status","server","connection","type",
|
15
|
+
"charset","length","language","expires"]
|
16
|
+
|
17
|
+
# Takes an HttpRequest and HttpResponse object, plus any additional arguments
|
18
|
+
# normally passed to CGI. These are used internally to create a wrapper around
|
19
|
+
# the real CGI while maintaining Mongrel's view of the world.
|
20
|
+
def initialize(request, response, *args)
|
21
|
+
@request = request
|
22
|
+
@response = response
|
23
|
+
@args = *args
|
24
|
+
@input = request.body
|
25
|
+
@head = {}
|
26
|
+
@out_called = false
|
27
|
+
@default_really_final = true
|
28
|
+
super(*args)
|
29
|
+
end
|
30
|
+
|
31
|
+
# The header is typically called to send back the header. In our case we
|
32
|
+
# collect it into a hash for later usage.
|
33
|
+
#
|
34
|
+
# nph -- Mostly ignored. It'll output the date.
|
35
|
+
# connection -- Completely ignored. Why is CGI doing this?
|
36
|
+
# length -- Ignored since Mongrel figures this out from what you write to output.
|
37
|
+
#
|
38
|
+
def header(options = "text/html")
|
39
|
+
# if they pass in a string then just write the Content-Type
|
40
|
+
if options.class == String
|
41
|
+
@head['Content-Type'] = options unless @head['Content-Type']
|
42
|
+
else
|
43
|
+
# convert the given options into what Mongrel wants
|
44
|
+
@head['Content-Type'] = options['type'] || "text/html"
|
45
|
+
@head['Content-Type'] += "; charset=" + options['charset'] if options.has_key? "charset" if options['charset']
|
46
|
+
|
47
|
+
# setup date only if they use nph
|
48
|
+
@head['Date'] = CGI::rfc1123_date(Time.now) if options['nph']
|
49
|
+
|
50
|
+
# setup the server to use the default or what they set
|
51
|
+
@head['Server'] = options['server'] || env_table['SERVER_SOFTWARE']
|
52
|
+
|
53
|
+
# remaining possible options they can give
|
54
|
+
@head['Status'] = options['status'] if options['status']
|
55
|
+
@head['Content-Language'] = options['language'] if options['language']
|
56
|
+
@head['Expires'] = options['expires'] if options['expires']
|
57
|
+
|
58
|
+
# drop the keys we don't want anymore
|
59
|
+
REMOVED_KEYS.each {|k| options.delete(k) }
|
60
|
+
|
61
|
+
# finally just convert the rest raw (which puts 'cookie' directly)
|
62
|
+
# 'cookie' is translated later as we write the header out
|
63
|
+
options.each{|k,v| @head[k] = v}
|
64
|
+
end
|
65
|
+
|
66
|
+
# doing this fakes out the cgi library to think the headers are empty
|
67
|
+
# we then do the real headers in the out function call later
|
68
|
+
""
|
69
|
+
end
|
70
|
+
|
71
|
+
# Takes any 'cookie' setting and sends it over the Mongrel header,
|
72
|
+
# then removes the setting from the options. If cookie is an
|
73
|
+
# Array or Hash then it sends those on with .to_s, otherwise
|
74
|
+
# it just calls .to_s on it and hopefully your "cookie" can
|
75
|
+
# write itself correctly.
|
76
|
+
def send_cookies(to)
|
77
|
+
# convert the cookies based on the myriad of possible ways to set a cookie
|
78
|
+
if @head['cookie']
|
79
|
+
cookie = @head['cookie']
|
80
|
+
case cookie
|
81
|
+
when Array
|
82
|
+
cookie.each {|c| to['Set-Cookie'] = c.to_s }
|
83
|
+
when Hash
|
84
|
+
cookie.each_value {|c| to['Set-Cookie'] = c.to_s}
|
85
|
+
else
|
86
|
+
to['Set-Cookie'] = options['cookie'].to_s
|
87
|
+
end
|
88
|
+
|
89
|
+
@head.delete('cookie')
|
90
|
+
end
|
91
|
+
|
92
|
+
# @output_cookies seems to never be used, but we'll process it just in case
|
93
|
+
@output_cookies.each {|c| to['Set-Cookie'] = c.to_s } if @output_cookies
|
94
|
+
end
|
95
|
+
|
96
|
+
# The dumb thing is people can call header or this or both and in any order.
|
97
|
+
# So, we just reuse header and then finalize the HttpResponse the right way.
|
98
|
+
# Status is taken from the various options and converted to what Mongrel needs
|
99
|
+
# via the CGIWrapper.status function.
|
100
|
+
#
|
101
|
+
# We also prevent Rails from actually doing the final send by adding a
|
102
|
+
# second parameter "really_final". Only Mongrel calls this after Rails
|
103
|
+
# is done. Since this will break other frameworks, it defaults to
|
104
|
+
# a different setting for rails (false) and (true) for others.
|
105
|
+
def out(options = "text/html", really_final=@default_really_final)
|
106
|
+
if @out_called || !really_final
|
107
|
+
# don't do it more than once or if it's not the really final call
|
108
|
+
return
|
109
|
+
end
|
110
|
+
|
111
|
+
header(options)
|
112
|
+
|
113
|
+
@response.start status do |head, body|
|
114
|
+
send_cookies(head)
|
115
|
+
|
116
|
+
@head.each {|k,v| head[k] = v}
|
117
|
+
body.write(yield || "")
|
118
|
+
end
|
119
|
+
|
120
|
+
@out_called = true
|
121
|
+
end
|
122
|
+
|
123
|
+
# Computes the status once, but lazily so that people who call header twice
|
124
|
+
# don't get penalized. Because CGI insists on including the options status
|
125
|
+
# message in the status we have to do a bit of parsing.
|
126
|
+
def status
|
127
|
+
if not @status
|
128
|
+
stat = @head["Status"]
|
129
|
+
stat = stat.split(' ')[0] if stat
|
130
|
+
|
131
|
+
@status = stat || "200"
|
132
|
+
end
|
133
|
+
|
134
|
+
@status
|
135
|
+
end
|
136
|
+
|
137
|
+
# Used to wrap the normal args variable used inside CGI.
|
138
|
+
def args
|
139
|
+
@args
|
140
|
+
end
|
141
|
+
|
142
|
+
# Used to wrap the normal env_table variable used inside CGI.
|
143
|
+
def env_table
|
144
|
+
@request.params
|
145
|
+
end
|
146
|
+
|
147
|
+
# Used to wrap the normal stdinput variable used inside CGI.
|
148
|
+
def stdinput
|
149
|
+
@input
|
150
|
+
end
|
151
|
+
|
152
|
+
# The stdoutput should be completely bypassed but we'll drop a warning just in case
|
153
|
+
def stdoutput
|
154
|
+
STDERR.puts "WARNING: Your program is doing something not expected. Please tell Zed that stdoutput was used and what software you are running. Thanks."
|
155
|
+
@response.body
|
156
|
+
end
|
157
|
+
|
158
|
+
end
|
159
|
+
end
|
data/lib/thin/cluster.rb
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
module Thin
|
2
|
+
# Control a set of servers. Generate start and stop commands and run them.
|
3
|
+
class Cluster
|
4
|
+
include Logging
|
5
|
+
|
6
|
+
attr_accessor :environment, :log_file, :pid_file, :user, :group, :timeout
|
7
|
+
attr_reader :address, :first_port, :size
|
8
|
+
|
9
|
+
# Script to run
|
10
|
+
def self.thin=(value)
|
11
|
+
@@thin = value
|
12
|
+
end
|
13
|
+
@@thin = 'thin'
|
14
|
+
|
15
|
+
# Create a new cluster of servers bound to +host+
|
16
|
+
# on ports +first_port+ to <tt>first_port + size - 1</tt>.
|
17
|
+
def initialize(dir, address, first_port, size)
|
18
|
+
@address = address
|
19
|
+
@first_port = first_port
|
20
|
+
@size = size
|
21
|
+
|
22
|
+
@log_file = 'thin.log'
|
23
|
+
@pid_file = 'thin.pid'
|
24
|
+
|
25
|
+
@timeout = 60 # sec
|
26
|
+
|
27
|
+
Dir.chdir dir if dir
|
28
|
+
end
|
29
|
+
|
30
|
+
# Start the servers
|
31
|
+
def start
|
32
|
+
with_each_instance do |port|
|
33
|
+
start_on_port port
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Start the server on a single port
|
38
|
+
def start_on_port(port)
|
39
|
+
logc "Starting #{address}:#{port} ... "
|
40
|
+
|
41
|
+
run :start, :port => port,
|
42
|
+
:address => @address,
|
43
|
+
:environment => @environment,
|
44
|
+
:daemonize => true,
|
45
|
+
:pid_file => pid_file_for(port),
|
46
|
+
:log_file => log_file_for(port),
|
47
|
+
:user => @user,
|
48
|
+
:group => @group,
|
49
|
+
:timeout => @timeout,
|
50
|
+
:trace => @trace
|
51
|
+
|
52
|
+
if wait_until_pid(:exist, port)
|
53
|
+
log "started in #{pid_for(port)}" if $?.success?
|
54
|
+
else
|
55
|
+
log 'failed to start'
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Stop the servers
|
60
|
+
def stop
|
61
|
+
with_each_instance do |port|
|
62
|
+
stop_on_port port
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Stop the server running on +port+
|
67
|
+
def stop_on_port(port)
|
68
|
+
logc "Stopping #{address}:#{port} ... "
|
69
|
+
|
70
|
+
run :stop, :pid_file => pid_file_for(port),
|
71
|
+
:timeout => @timeout
|
72
|
+
|
73
|
+
if wait_until_pid(!:exist, port)
|
74
|
+
log 'stopped' if $?.success?
|
75
|
+
else
|
76
|
+
log 'failed to stop'
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Restart the servers one at the time.
|
81
|
+
# Prevent downtime by making sure only one is stopped at the time.
|
82
|
+
# See http://blog.carlmercier.com/2007/09/07/a-better-approach-to-restarting-a-mongrel-cluster/
|
83
|
+
def restart
|
84
|
+
with_each_instance do |port|
|
85
|
+
stop_on_port port
|
86
|
+
sleep 0.1 # Let the OS do his thang
|
87
|
+
start_on_port port
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def log_file_for(port)
|
92
|
+
include_port_number @log_file, port
|
93
|
+
end
|
94
|
+
|
95
|
+
def pid_file_for(port)
|
96
|
+
include_port_number @pid_file, port
|
97
|
+
end
|
98
|
+
|
99
|
+
def pid_for(port)
|
100
|
+
File.read(pid_file_for(port)).chomp.to_i
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
# Send the command to the +thin+ script
|
105
|
+
def run(cmd, options={})
|
106
|
+
shell_cmd = shellify(cmd, options)
|
107
|
+
trace shell_cmd
|
108
|
+
`#{shell_cmd}`
|
109
|
+
end
|
110
|
+
|
111
|
+
# Turn into a runnable shell command
|
112
|
+
def shellify(cmd, options={})
|
113
|
+
shellified_options = options.inject([]) do |args, (name, value)|
|
114
|
+
args << case value
|
115
|
+
when NilClass
|
116
|
+
when TrueClass then "--#{name}"
|
117
|
+
else "--#{name.to_s.tr('_', '-')}=#{value.inspect}"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
"#{@@thin} #{cmd} #{shellified_options.compact.join(' ')}"
|
121
|
+
end
|
122
|
+
|
123
|
+
# Wait for the pid file to be created (exist=true) of deleted (exist=false)
|
124
|
+
def wait_until_pid(exist, port)
|
125
|
+
Timeout.timeout(@timeout) do
|
126
|
+
sleep 0.1 until File.exist?(pid_file_for(port)) == !!exist
|
127
|
+
end
|
128
|
+
true
|
129
|
+
rescue Timeout::Error
|
130
|
+
false
|
131
|
+
end
|
132
|
+
|
133
|
+
def with_each_instance
|
134
|
+
@size.times do |n|
|
135
|
+
port = @first_port + n
|
136
|
+
yield port
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# Add the port numbers in the filename
|
141
|
+
# so each instance get its own file
|
142
|
+
def include_port_number(path, port)
|
143
|
+
raise ArgumentError, "filename '#{path}' must include an extension" unless path =~ /\./
|
144
|
+
path.gsub(/\.(.+)$/) { ".#{port}.#{$1}" }
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
data/lib/thin/command.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'transat/parser'
|
2
|
+
|
3
|
+
module Thin
|
4
|
+
# Define a set of commands that can be parsed and executed.
|
5
|
+
# see bin/thin for an example.
|
6
|
+
def self.define_commands(&block)
|
7
|
+
begin
|
8
|
+
Transat::Parser.parse_and_execute(ARGV, &block)
|
9
|
+
rescue CommandError => e
|
10
|
+
puts "Error: #{e}"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Raised when a command specific error happen.
|
15
|
+
class CommandError < StandardError; end
|
16
|
+
|
17
|
+
# A command that can be runned from a command line script.
|
18
|
+
class Command
|
19
|
+
attr_reader :args
|
20
|
+
|
21
|
+
def initialize(non_options, options)
|
22
|
+
@args = non_options
|
23
|
+
|
24
|
+
options.each do |option, value|
|
25
|
+
self.send("#{option}=", value)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.command_name
|
30
|
+
self.name.match(/::(\w+)$/)[1].downcase
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.detailed_help
|
34
|
+
<<-EOF
|
35
|
+
usage: #{File.basename($PROGRAM_NAME)} #{command_name}
|
36
|
+
|
37
|
+
#{help}
|
38
|
+
EOF
|
39
|
+
end
|
40
|
+
|
41
|
+
protected
|
42
|
+
def error(message)
|
43
|
+
raise CommandError, message
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
module Commands; end
|
48
|
+
Dir[File.dirname(__FILE__) + '/commands/**/*.rb'].each { |l| require l }
|
49
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'thin/cluster'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
module Thin::Commands::Cluster
|
5
|
+
class Base < Thin::Command
|
6
|
+
def self.config_attributes
|
7
|
+
[:address, :port, :environment, :log_file, :pid_file, :cwd, :servers, :user, :group]
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_accessor *self.config_attributes
|
11
|
+
attr_accessor :config, :trace
|
12
|
+
|
13
|
+
protected
|
14
|
+
def load_from_config
|
15
|
+
return unless File.exist?(config)
|
16
|
+
|
17
|
+
hash = File.open(config) { |file| YAML.load(file) }
|
18
|
+
|
19
|
+
self.class.config_attributes.each do |attr|
|
20
|
+
send "#{attr}=", hash[attr.to_s]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Thin::Commands::Cluster
|
2
|
+
class Config < Base
|
3
|
+
def run
|
4
|
+
error 'Config file required' unless config
|
5
|
+
|
6
|
+
Dir.chdir cwd if cwd
|
7
|
+
|
8
|
+
hash = {}
|
9
|
+
self.class.config_attributes.each do |attr|
|
10
|
+
hash[attr.to_s] = send(attr)
|
11
|
+
end
|
12
|
+
|
13
|
+
File.open(config, 'w') { |f| f << YAML.dump(hash) }
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.help
|
17
|
+
"Create a thin_cluster configuration file."
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.detailed_help
|
21
|
+
<<-EOF
|
22
|
+
usage: thin_cluster config [options]
|
23
|
+
|
24
|
+
Create a configuration file for thin_cluster.
|
25
|
+
|
26
|
+
All the options passed to this command will be stored
|
27
|
+
in <config> in YAML format.
|
28
|
+
|
29
|
+
You can then use this configuration file with the start,
|
30
|
+
stop and restart commands with the --config option.
|
31
|
+
EOF
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Thin::Commands::Cluster
|
2
|
+
class Restart < Base
|
3
|
+
def run
|
4
|
+
load_from_config
|
5
|
+
|
6
|
+
cluster = Thin::Cluster.new(cwd, address, port, servers)
|
7
|
+
|
8
|
+
cluster.log_file = log_file
|
9
|
+
cluster.pid_file = pid_file
|
10
|
+
cluster.trace = trace
|
11
|
+
cluster.user = user
|
12
|
+
cluster.group = group
|
13
|
+
|
14
|
+
cluster.restart
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.help
|
18
|
+
"Restart servers"
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.detailed_help
|
22
|
+
<<-EOF
|
23
|
+
usage: thin_cluster restart [options]
|
24
|
+
|
25
|
+
Restart the servers one at the time.
|
26
|
+
Prevent downtime by making sure only one is stopped at the time.
|
27
|
+
|
28
|
+
For example, first server is stopped, then started. When the first
|
29
|
+
server is fully started the second one is stopped ...
|
30
|
+
|
31
|
+
See http://blog.carlmercier.com/2007/09/07/a-better-approach-to-restarting-a-mongrel-cluster/
|
32
|
+
EOF
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|