thin 0.6.2-x86-mswin32-60 → 0.6.3-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.

Files changed (48) hide show
  1. data/CHANGELOG +34 -1
  2. data/bin/thin +2 -164
  3. data/example/config.ru +4 -1
  4. data/example/ramaze.ru +12 -0
  5. data/example/thin.god +70 -66
  6. data/lib/rack/adapter/rails.rb +0 -3
  7. data/lib/rack/handler/thin.rb +6 -1
  8. data/lib/thin.rb +13 -4
  9. data/lib/thin/command.rb +9 -5
  10. data/lib/thin/connection.rb +5 -14
  11. data/lib/thin/connectors/connector.rb +61 -0
  12. data/lib/thin/connectors/tcp_server.rb +29 -0
  13. data/lib/thin/connectors/unix_server.rb +48 -0
  14. data/lib/thin/controllers/cluster.rb +115 -0
  15. data/lib/thin/controllers/controller.rb +85 -0
  16. data/lib/thin/controllers/service.rb +73 -0
  17. data/lib/thin/controllers/service.sh.erb +39 -0
  18. data/lib/thin/daemonizing.rb +9 -4
  19. data/lib/thin/headers.rb +2 -2
  20. data/lib/thin/runner.rb +166 -0
  21. data/lib/thin/server.rb +109 -89
  22. data/lib/thin/stats.html.erb +216 -0
  23. data/lib/thin/stats.rb +1 -249
  24. data/lib/thin/version.rb +10 -3
  25. data/lib/thin_parser.so +0 -0
  26. data/spec/command_spec.rb +0 -1
  27. data/spec/configs/cluster.yml +9 -0
  28. data/spec/configs/single.yml +9 -0
  29. data/spec/{cluster_spec.rb → controllers/cluster_spec.rb} +22 -10
  30. data/spec/controllers/controller_spec.rb +85 -0
  31. data/spec/controllers/service_spec.rb +51 -0
  32. data/spec/daemonizing_spec.rb +73 -9
  33. data/spec/request/mongrel_spec.rb +39 -0
  34. data/spec/{request_spec.rb → request/parser_spec.rb} +11 -143
  35. data/spec/request/perf_spec.rb +50 -0
  36. data/spec/request/processing_spec.rb +46 -0
  37. data/spec/runner_spec.rb +135 -0
  38. data/spec/server/builder_spec.rb +38 -0
  39. data/spec/server/stopping_spec.rb +45 -0
  40. data/spec/server/tcp_spec.rb +54 -0
  41. data/spec/server/unix_socket_spec.rb +30 -0
  42. data/spec/spec_helper.rb +49 -16
  43. data/tasks/announce.rake +7 -3
  44. data/tasks/email.erb +8 -18
  45. data/tasks/stats.rake +21 -8
  46. metadata +33 -7
  47. data/lib/thin/cluster.rb +0 -123
  48. data/spec/server_spec.rb +0 -200
@@ -0,0 +1,39 @@
1
+ #!/bin/sh
2
+ ### BEGIN INIT INFO
3
+ # Provides: thin
4
+ # Required-Start: $local_fs $remote_fs
5
+ # Required-Stop: $local_fs $remote_fs
6
+ # Default-Start: 2 3 4 5
7
+ # Default-Stop: S 0 1 6
8
+ # Short-Description: thin initscript
9
+ # Description: thin
10
+ ### END INIT INFO
11
+
12
+ # Original author: Forrest Robertson
13
+
14
+ # Do NOT "set -e"
15
+
16
+ DAEMON=<%= Command.script %>
17
+ SCRIPT_NAME=<%= INITD_PATH %>
18
+ CONFIG_PATH=<%= config_files_path %>
19
+
20
+ # Exit if the package is not installed
21
+ [ -x "$DAEMON" ] || exit 0
22
+
23
+ case "$1" in
24
+ start)
25
+ $DAEMON start --all $CONFIG_PATH
26
+ ;;
27
+ stop)
28
+ $DAEMON stop --all $CONFIG_PATH
29
+ ;;
30
+ restart)
31
+ $DAEMON restart --all $CONFIG_PATH
32
+ ;;
33
+ *)
34
+ echo "Usage: $SCRIPT_NAME {start|stop|restart}" >&2
35
+ exit 3
36
+ ;;
37
+ esac
38
+
39
+ :
@@ -12,6 +12,9 @@ module Process
12
12
  end
13
13
 
14
14
  module Thin
15
+ # Raised when the pid file already exist starting as a daemon.
16
+ class PidFileExist < RuntimeError; end
17
+
15
18
  # Module included in classes that can be turned into a daemon.
16
19
  # Handle stuff like:
17
20
  # * storing the PID in a file
@@ -31,15 +34,17 @@ module Thin
31
34
 
32
35
  # Turns the current script into a daemon process that detaches from the console.
33
36
  def daemonize
34
- raise PlatformNotSupported, 'Daemonizing not supported on Windows' if Thin.win?
35
- raise ArgumentError, 'You must specify a pid_file to deamonize' unless @pid_file
37
+ raise PlatformNotSupported, 'Daemonizing not supported on Windows' if Thin.win?
38
+ raise ArgumentError, 'You must specify a pid_file to daemonize' unless @pid_file
39
+ raise PidFileExist, "#{@pid_file} already exist, seems like it's already running. " +
40
+ "Stop the process or delete #{@pid_file}." if File.exist?(@pid_file)
36
41
 
37
42
  pwd = Dir.pwd # Current directory is changed during daemonization, so store it
38
43
 
39
- Daemonize.daemonize(File.expand_path(@log_file))
44
+ Daemonize.daemonize(File.expand_path(@log_file), name)
40
45
 
41
46
  Dir.chdir(pwd)
42
-
47
+
43
48
  write_pid_file
44
49
 
45
50
  trap('HUP') { restart }
@@ -7,7 +7,7 @@ module Thin
7
7
 
8
8
  def initialize
9
9
  @sent = {}
10
- @out = ''
10
+ @out = []
11
11
  end
12
12
 
13
13
  # Add <tt>key: value</tt> pair to the headers.
@@ -21,7 +21,7 @@ module Thin
21
21
  end
22
22
 
23
23
  def to_s
24
- @out
24
+ @out.join
25
25
  end
26
26
  end
27
27
  end
@@ -0,0 +1,166 @@
1
+ require 'optparse'
2
+ require 'yaml'
3
+
4
+ module Thin
5
+ # CLI runner.
6
+ # Parse options and send command to the correct Controller.
7
+ class Runner
8
+ COMMANDS = %w(start stop restart config)
9
+ LINUX_ONLY_COMMANDS = %w(install)
10
+
11
+ # Commands that wont load options from the config file
12
+ CONFIGLESS_COMMANDS = %w(config install)
13
+
14
+ # Parsed options
15
+ attr_accessor :options
16
+
17
+ # Name of the command to be runned.
18
+ attr_accessor :command
19
+
20
+ # Arguments to be passed to the command.
21
+ attr_accessor :arguments
22
+
23
+ # Return all available commands
24
+ def self.commands
25
+ commands = COMMANDS
26
+ commands += LINUX_ONLY_COMMANDS if Thin.linux?
27
+ commands
28
+ end
29
+
30
+ def initialize(argv)
31
+ @argv = argv
32
+
33
+ # Default options values
34
+ @options = {
35
+ :chdir => Dir.pwd,
36
+ :environment => 'development',
37
+ :address => '0.0.0.0',
38
+ :port => 3000,
39
+ :timeout => 60,
40
+ :log => 'log/thin.log',
41
+ :pid => 'tmp/pids/thin.pid'
42
+ }
43
+
44
+ parse!
45
+ end
46
+
47
+ def parser
48
+ # NOTE: If you add an option here make sure the key in the +options+ hash is the
49
+ # same as the name of the command line option.
50
+ # +option+ keys are used to build the command line to launch other processes,
51
+ # see <tt>lib/thin/command.rb</tt>.
52
+ @parser ||= OptionParser.new do |opts|
53
+ opts.banner = "Usage: thin [options] #{self.class.commands.join('|')}"
54
+
55
+ opts.separator ""
56
+ opts.separator "Server options:"
57
+
58
+ opts.on("-a", "--address HOST", "bind to HOST address " +
59
+ "(default: #{@options[:address]})") { |host| @options[:address] = host }
60
+ opts.on("-p", "--port PORT", "use PORT (default: #{@options[:port]})") { |port| @options[:port] = port.to_i }
61
+ opts.on("-S", "--socket FILE", "bind to unix domain socket") { |file| @options[:socket] = file }
62
+ opts.on("-e", "--environment ENV", "Rails environment " +
63
+ "(default: #{@options[:environment]})") { |env| @options[:environment] = env }
64
+ opts.on("-c", "--chdir DIR", "Change to dir before starting") { |dir| @options[:chdir] = File.expand_path(dir) }
65
+ opts.on("-t", "--timeout SEC", "Request or command timeout in sec " +
66
+ "(default: #{@options[:timeout]})") { |sec| @options[:timeout] = sec.to_i }
67
+ opts.on("-r", "--rackup FILE", "Load a Rack config file instead of " +
68
+ "the Rails adapter") { |file| @options[:rackup] = file }
69
+ opts.on( "--prefix PATH", "Mount the app under PATH (start with /)") { |path| @options[:prefix] = path }
70
+ opts.on( "--stats PATH", "Mount the Stats adapter under PATH") { |path| @options[:stats] = path }
71
+
72
+ unless Thin.win? # Daemonizing not supported on Windows
73
+ opts.separator ""
74
+ opts.separator "Daemon options:"
75
+
76
+ opts.on("-d", "--daemonize", "Run daemonized in the background") { @options[:daemonize] = true }
77
+ opts.on("-l", "--log FILE", "File to redirect output " +
78
+ "(default: #{@options[:log]})") { |file| @options[:log] = file }
79
+ opts.on("-P", "--pid FILE", "File to store PID " +
80
+ "(default: #{@options[:pid]})") { |file| @options[:pid] = file }
81
+ opts.on("-u", "--user NAME", "User to run daemon as (use with -g)") { |user| @options[:user] = user }
82
+ opts.on("-g", "--group NAME", "Group to run daemon as (use with -u)") { |group| @options[:group] = group }
83
+
84
+ opts.separator ""
85
+ opts.separator "Cluster options:"
86
+
87
+ opts.on("-s", "--servers NUM", "Number of servers to start") { |num| @options[:servers] = num.to_i }
88
+ opts.on("-o", "--only NUM", "Send command to only one server of the cluster") { |only| @options[:only] = only }
89
+ opts.on("-C", "--config FILE", "Load options from config file") { |file| @options[:config] = file }
90
+ opts.on( "--all [DIR]", "Send command to each config files in DIR") { |dir| @options[:all] = dir } if Thin.linux?
91
+ end
92
+
93
+ opts.separator ""
94
+ opts.separator "Common options:"
95
+
96
+ opts.on_tail("-D", "--debug", "Set debbuging on") { $DEBUG = true }
97
+ opts.on_tail("-V", "--trace", "Set tracing on") { $TRACE = true }
98
+ opts.on_tail("-h", "--help", "Show this message") { puts opts; exit }
99
+ opts.on_tail('-v', '--version', "Show version") { puts Thin::SERVER; exit }
100
+ end
101
+ end
102
+
103
+ # Parse the options.
104
+ def parse!
105
+ parser.parse! @argv
106
+ @command = @argv.shift
107
+ @arguments = @argv
108
+ end
109
+
110
+ # Parse the current shell arguments and run the command.
111
+ # Exits on error.
112
+ def run!
113
+ if self.class.commands.include?(@command)
114
+ run_command
115
+ elsif @command.nil?
116
+ puts "Command required"
117
+ puts @parser
118
+ exit 1
119
+ else
120
+ abort "Invalid command: #{@command}"
121
+ end
122
+ end
123
+
124
+ # Send the command to the controller: single instance or cluster.
125
+ def run_command
126
+ load_options_from_config_file! unless CONFIGLESS_COMMANDS.include?(@command)
127
+
128
+ # PROGRAM_NAME is relative to the current directory, so make sure
129
+ # we store and expand it before changing directory.
130
+ Command.script = File.expand_path($PROGRAM_NAME)
131
+
132
+ # Change the current directory ASAP so that all relative paths are
133
+ # relative to this one.
134
+ Dir.chdir(@options[:chdir]) unless CONFIGLESS_COMMANDS.include?(@command)
135
+
136
+ controller = case
137
+ when cluster? then Controllers::Cluster.new(@options)
138
+ when service? then Controllers::Service.new(@options)
139
+ else Controllers::Controller.new(@options)
140
+ end
141
+
142
+ if controller.respond_to?(@command)
143
+ controller.send(@command, *@arguments)
144
+ else
145
+ abort "Invalid options for command: #{@command}"
146
+ end
147
+ end
148
+
149
+ # +true+ if we're controlling a cluster.
150
+ def cluster?
151
+ @options[:only] || @options[:servers]
152
+ end
153
+
154
+ # +true+ if we're acting a as system service.
155
+ def service?
156
+ @options.has_key?(:all) || @command == 'install'
157
+ end
158
+
159
+ private
160
+ def load_options_from_config_file!
161
+ if file = @options.delete(:config)
162
+ YAML.load_file(file).each { |key, value| @options[key.to_sym] = value }
163
+ end
164
+ end
165
+ end
166
+ end
@@ -1,68 +1,106 @@
1
1
  module Thin
2
- # The Thin HTTP server used to served request.
3
- # It listen for incoming request on a given port
2
+ # The uterly famous Thin HTTP server.
3
+ # It listen for incoming request through a given connector
4
4
  # and forward all request to +app+.
5
5
  #
6
- # Based on HTTP 1.1 protocol specs:
7
- # http://www.w3.org/Protocols/rfc2616/rfc2616.html
6
+ # == TCP server
7
+ # Create a new TCP server on bound to <tt>host:port</tt> by specifiying +host+
8
+ # and +port+ as the first 2 arguments.
9
+ #
10
+ # Thin::Server.start('0.0.0.0', 3000, app)
11
+ #
12
+ # == UNIX domain server
13
+ # Create a new UNIX domain socket bound to +socket+ file by specifiying a filename
14
+ # as the first argument. Eg.: /tmp/thin.sock. If the first argument contains a <tt>/</tt>
15
+ # it will be assumed to be a UNIX socket.
16
+ #
17
+ # Thin::Server.start('/tmp/thin.sock', nil, app)
18
+ #
19
+ # == Using a custom connector
20
+ # You can implement your own way to connect the server to its client by creating your
21
+ # own Thin::Connectors::Connector class and pass it as the first argument.
22
+ #
23
+ # connector = Thin::Connectors::MyFancyConnector.new('galaxy://faraway:1345')
24
+ # Thin::Server.start(connector, nil, app)
25
+ #
26
+ # == Rack application (+app+)
27
+ # All requests will be processed through +app+ that must be a valid Rack adapter.
28
+ # A valid Rack adapter (application) must respond to <tt>call(env#Hash)</tt> and
29
+ # return an array of <tt>[status, headers, body]</tt>.
30
+ #
31
+ # == Building an app in place
32
+ # If a block is passed, a <tt>Rack::Builder</tt> instance
33
+ # will be passed to build the +app+. So you can do cool stuff like this:
34
+ #
35
+ # Thin::Server.start('0.0.0.0', 3000) do
36
+ # use Rack::CommonLogger
37
+ # use Rack::ShowExceptions
38
+ # map "/lobster" do
39
+ # use Rack::Lint
40
+ # run Rack::Lobster.new
41
+ # end
42
+ # end
43
+ #
8
44
  class Server
9
45
  include Logging
10
46
  include Daemonizable
47
+ extend Forwardable
48
+
49
+ # Application (Rack adapter) called with the request that produces the response.
50
+ attr_accessor :app
11
51
 
12
- # Address and port on which the server is listening for connections.
13
- attr_accessor :port, :host
52
+ # Connector handling the connections to the clients.
53
+ attr_accessor :connector
14
54
 
15
- # UNIX domain socket on which the server is listening for connections.
16
- attr_accessor :socket
55
+ # Maximum number of seconds for incoming data to arrive before the connection
56
+ # is dropped.
57
+ def_delegators :@connector, :timeout, :timeout=
17
58
 
18
- # App called with the request that produces the response.
19
- attr_accessor :app
59
+ # Address and port on which the server is listening for connections.
60
+ def_delegators :@connector, :host, :port
20
61
 
21
- # Maximum time for incoming data to arrive
22
- attr_accessor :timeout
62
+ # UNIX domain socket on which the server is listening for connections.
63
+ def_delegator :@connector, :socket
23
64
 
24
- # Creates a new server bound to <tt>host:port</tt>
25
- # or to +socket+ that will pass request to +app+.
26
- # If +host_or_socket+ contains a <tt>/</tt> it is assumed
27
- # to be a UNIX domain socket filename.
28
- # If a block is passed, a <tt>Rack::Builder</tt> instance
29
- # will be passed to build the +app+.
30
- #
31
- # Server.new '0.0.0.0', 3000 do
32
- # use Rack::CommonLogger
33
- # use Rack::ShowExceptions
34
- # map "/lobster" do
35
- # use Rack::Lint
36
- # run Rack::Lobster.new
37
- # end
38
- # end.start
39
- #
40
- def initialize(host_or_socket, port=3000, app=nil, &block)
41
- if host_or_socket.include?('/')
42
- @socket = host_or_socket
43
- else
44
- @host = host_or_socket
45
- @port = port.to_i
46
- end
47
- @app = app
48
- @timeout = 60 # sec
49
- @connections = []
65
+ def initialize(host_or_socket_or_connector, port=3000, app=nil, &block)
66
+ # Try to intelligently select which connector to use.
67
+ @connector = case
68
+ when host_or_socket_or_connector.is_a?(Connectors::Connector)
69
+ host_or_socket_or_connector
70
+ when host_or_socket_or_connector.include?('/')
71
+ Connectors::UnixServer.new(host_or_socket_or_connector)
72
+ else
73
+ Connectors::TcpServer.new(host_or_socket_or_connector, port.to_i)
74
+ end
75
+
76
+ @connector.server = self
77
+ @app = app
50
78
 
79
+ # Allow using Rack builder as a block
51
80
  @app = Rack::Builder.new(&block).to_app if block
52
81
  end
53
82
 
83
+ # Lil' shortcut to turn this:
84
+ #
85
+ # Server.new(...).start
86
+ #
87
+ # into this:
88
+ #
89
+ # Server.start(...)
90
+ #
54
91
  def self.start(*args, &block)
55
92
  new(*args, &block).start!
56
93
  end
57
94
 
58
- # Start the server and listen for connections
95
+ # Start the server and listen for connections.
96
+ # Also register signals:
97
+ # * INT calls +stop+ to shutdown gracefully.
98
+ # * TERM calls <tt>stop!</tt> to force shutdown.
59
99
  def start
60
100
  raise ArgumentError, 'app required' unless @app
61
101
 
62
102
  trap('INT') { stop }
63
103
  trap('TERM') { stop! }
64
-
65
- at_exit { remove_socket_file } if @socket
66
104
 
67
105
  # See http://rubyeventmachine.com/pub/rdoc/files/EPOLL.html
68
106
  EventMachine.epoll
@@ -70,83 +108,65 @@ module Thin
70
108
  log ">> Thin web server (v#{VERSION::STRING} codename #{VERSION::CODENAME})"
71
109
  trace ">> Tracing ON"
72
110
 
73
- EventMachine.run { @signature = start_server }
111
+ log ">> Listening on #{@connector}, CTRL+C to stop"
112
+ @running = true
113
+ EventMachine.run { @connector.connect }
74
114
  end
75
115
  alias :start! :start
76
116
 
117
+ # == Gracefull shutdown
77
118
  # Stops the server after processing all current connections.
119
+ # As soon as this method is called, the server stops accepting
120
+ # new requests and wait for all current connections to finish.
78
121
  # Calling twice is the equivalent of calling <tt>stop!</tt>.
79
122
  def stop
80
- if @stopping
81
- stop!
82
- else
83
- @stopping = true
123
+ if @running
124
+ @running = false
84
125
 
85
126
  # Do not accept anymore connection
86
- EventMachine.stop_server(@signature)
127
+ @connector.disconnect
87
128
 
88
129
  unless wait_for_connections_and_stop
89
130
  # Still some connections running, schedule a check later
90
131
  EventMachine.add_periodic_timer(1) { wait_for_connections_and_stop }
91
132
  end
133
+ else
134
+ stop!
92
135
  end
93
136
  end
94
137
 
95
- # Stops the server closing all current connections
138
+ # == Force shutdown
139
+ # Stops the server closing all current connections right away.
140
+ # This doesn't wait for connection to finish their work and send data.
141
+ # All current requests will be dropped.
96
142
  def stop!
97
143
  log ">> Stopping ..."
98
144
 
99
- @connections.each { |connection| connection.close_connection }
145
+ @connector.close_connections
100
146
  EventMachine.stop
101
147
 
102
- remove_socket_file
148
+ @connector.close
103
149
  end
150
+
151
+ def name
152
+ "thin server (#{@connector})"
153
+ end
154
+ alias :to_s :name
104
155
 
105
- def connection_finished(connection)
106
- @connections.delete(connection)
156
+ # Return +true+ if the server is running and ready to receive requests.
157
+ # Note that the server might still be running and return +false+ when
158
+ # shuting down and waiting for active connections to complete.
159
+ def running?
160
+ @running
107
161
  end
108
162
 
109
- protected
110
- def start_server
111
- if @socket
112
- start_server_on_socket
113
- else
114
- start_server_on_host
115
- end
116
- end
117
-
118
- def start_server_on_host
119
- log ">> Listening on #{@host}:#{@port}, CTRL+C to stop"
120
- EventMachine.start_server(@host, @port, Connection, &method(:initialize_connection))
121
- end
122
-
123
- def start_server_on_socket
124
- raise PlatformNotSupported, 'UNIX sockets not available on Windows' if Thin.win?
125
-
126
- log ">> Listening on #{@socket}, CTRL+C to stop"
127
- EventMachine.start_unix_domain_server(@socket, Connection, &method(:initialize_connection))
128
- end
129
-
130
- def initialize_connection(connection)
131
- connection.server = self
132
- connection.comm_inactivity_timeout = @timeout
133
- connection.app = @app
134
- connection.silent = @silent
135
- connection.unix_socket = !@socket.nil?
136
-
137
- @connections << connection
138
- end
139
-
140
- def remove_socket_file
141
- File.delete(@socket) if @socket && File.exist?(@socket)
142
- end
143
-
163
+ protected
144
164
  def wait_for_connections_and_stop
145
- if @connections.empty?
165
+ if @connector.empty?
146
166
  stop!
147
167
  true
148
168
  else
149
- log ">> Waiting for #{@connections.size} connection(s) to finish, CTRL+C to force stop"
169
+ log ">> Waiting for #{@connector.size} connection(s) to finish, CTRL+C to force stop"
150
170
  false
151
171
  end
152
172
  end