thin 0.5.0 → 0.5.2

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 (67) hide show
  1. data/COPYING +1 -1
  2. data/README +3 -7
  3. data/Rakefile +6 -163
  4. data/bin/thin +87 -48
  5. data/example/thin.god +72 -0
  6. data/ext/thin_parser/thin.c +7 -7
  7. data/lib/rack/adapter/rails.rb +38 -22
  8. data/lib/thin.rb +2 -1
  9. data/lib/thin/cluster.rb +106 -0
  10. data/lib/thin/connection.rb +3 -4
  11. data/lib/thin/daemonizing.rb +4 -24
  12. data/lib/thin/request.rb +6 -5
  13. data/lib/thin/response.rb +1 -3
  14. data/lib/thin/server.rb +9 -6
  15. data/lib/thin/version.rb +6 -4
  16. data/lib/thin_parser.bundle +0 -0
  17. data/spec/cluster_spec.rb +58 -0
  18. data/spec/daemonizing_spec.rb +1 -2
  19. data/spec/rack_rails_spec.rb +73 -0
  20. data/spec/rails_app/app/controllers/application.rb +10 -0
  21. data/spec/rails_app/app/controllers/simple_controller.rb +19 -0
  22. data/spec/rails_app/app/helpers/application_helper.rb +3 -0
  23. data/spec/rails_app/app/views/simple/index.html.erb +15 -0
  24. data/spec/rails_app/config/boot.rb +109 -0
  25. data/spec/rails_app/config/environment.rb +64 -0
  26. data/spec/rails_app/config/environments/development.rb +18 -0
  27. data/spec/rails_app/config/environments/production.rb +19 -0
  28. data/spec/rails_app/config/environments/test.rb +22 -0
  29. data/spec/rails_app/config/initializers/inflections.rb +10 -0
  30. data/spec/rails_app/config/initializers/mime_types.rb +5 -0
  31. data/spec/rails_app/config/routes.rb +35 -0
  32. data/spec/rails_app/public/404.html +30 -0
  33. data/spec/rails_app/public/422.html +30 -0
  34. data/spec/rails_app/public/500.html +30 -0
  35. data/spec/rails_app/public/dispatch.cgi +10 -0
  36. data/spec/rails_app/public/dispatch.fcgi +24 -0
  37. data/spec/rails_app/public/dispatch.rb +10 -0
  38. data/spec/rails_app/public/favicon.ico +0 -0
  39. data/spec/rails_app/public/images/rails.png +0 -0
  40. data/spec/rails_app/public/index.html +277 -0
  41. data/spec/rails_app/public/javascripts/application.js +2 -0
  42. data/spec/rails_app/public/javascripts/controls.js +963 -0
  43. data/spec/rails_app/public/javascripts/dragdrop.js +972 -0
  44. data/spec/rails_app/public/javascripts/effects.js +1120 -0
  45. data/spec/rails_app/public/javascripts/prototype.js +4225 -0
  46. data/spec/rails_app/public/robots.txt +5 -0
  47. data/spec/rails_app/script/about +3 -0
  48. data/spec/rails_app/script/console +3 -0
  49. data/spec/rails_app/script/destroy +3 -0
  50. data/spec/rails_app/script/generate +3 -0
  51. data/spec/rails_app/script/performance/benchmarker +3 -0
  52. data/spec/rails_app/script/performance/profiler +3 -0
  53. data/spec/rails_app/script/performance/request +3 -0
  54. data/spec/rails_app/script/plugin +3 -0
  55. data/spec/rails_app/script/process/inspector +3 -0
  56. data/spec/rails_app/script/process/reaper +3 -0
  57. data/spec/rails_app/script/process/spawner +3 -0
  58. data/spec/rails_app/script/runner +3 -0
  59. data/spec/rails_app/script/server +3 -0
  60. data/spec/response_spec.rb +16 -0
  61. data/spec/spec_helper.rb +1 -0
  62. metadata +71 -11
  63. data/doc/rdoc/created.rid +0 -1
  64. data/doc/rdoc/files/README.html +0 -197
  65. data/doc/rdoc/index.html +0 -10
  66. data/doc/rdoc/logo.gif +0 -0
  67. data/doc/rdoc/rdoc-style.css +0 -91
@@ -77,7 +77,7 @@ void http_field(void *data, const char *field, size_t flen, const char *value, s
77
77
  f = rb_str_dup(global_http_prefix);
78
78
  f = rb_str_buf_cat(f, field, flen);
79
79
 
80
- for(ch = RSTRING(f)->ptr, end = ch + RSTRING(f)->len; ch < end; ch++) {
80
+ for(ch = RSTRING_PTR(f), end = ch + RSTRING_LEN(f); ch < end; ch++) {
81
81
  if(*ch == '-') {
82
82
  *ch = '_';
83
83
  } else {
@@ -176,12 +176,12 @@ void header_done(void *data, const char *at, size_t length)
176
176
  rb_hash_aset(req, global_gateway_interface, global_gateway_interface_value);
177
177
  if((temp = rb_hash_aref(req, global_http_host)) != Qnil) {
178
178
  /* ruby better close strings off with a '\0' dammit */
179
- colon = strchr(RSTRING(temp)->ptr, ':');
179
+ colon = strchr(RSTRING_PTR(temp), ':');
180
180
  if(colon != NULL) {
181
- rb_hash_aset(req, global_server_name, rb_str_substr(temp, 0, colon - RSTRING(temp)->ptr));
181
+ rb_hash_aset(req, global_server_name, rb_str_substr(temp, 0, colon - RSTRING_PTR(temp)));
182
182
  rb_hash_aset(req, global_server_port,
183
- rb_str_substr(temp, colon - RSTRING(temp)->ptr+1,
184
- RSTRING(temp)->len));
183
+ rb_str_substr(temp, colon - RSTRING_PTR(temp)+1,
184
+ RSTRING_LEN(temp)));
185
185
  } else {
186
186
  rb_hash_aset(req, global_server_name, temp);
187
187
  rb_hash_aset(req, global_server_port, global_port_80);
@@ -313,8 +313,8 @@ VALUE HttpParser_execute(VALUE self, VALUE req_hash, VALUE data, VALUE start)
313
313
  DATA_GET(self, http_parser, http);
314
314
 
315
315
  from = FIX2INT(start);
316
- dptr = RSTRING(data)->ptr;
317
- dlen = RSTRING(data)->len;
316
+ dptr = RSTRING_PTR(data);
317
+ dlen = RSTRING_LEN(data);
318
318
 
319
319
  if(from >= dlen) {
320
320
  rb_raise(eHttpParserError, "Requested start is after data buffer end.");
@@ -14,8 +14,8 @@ module Rack
14
14
  module Adapter
15
15
  class Rails
16
16
  def initialize(options={})
17
- @root = options[:root] || Dir.pwd
18
- @env = options[:env] || 'development'
17
+ @root = options[:root] || Dir.pwd
18
+ @env = options[:environment] || 'development'
19
19
 
20
20
  load_application
21
21
 
@@ -30,15 +30,16 @@ module Rack
30
30
  end
31
31
 
32
32
  # TODO refactor this in File#can_serve?(path) ??
33
- def file?(path)
33
+ def file_exist?(path)
34
34
  full_path = ::File.join(@file_server.root, Utils.unescape(path))
35
35
  ::File.file?(full_path) && ::File.readable?(full_path)
36
36
  end
37
37
 
38
- def call(env)
39
- # Serve the file if it's there
40
- return @file_server.call(env) if file?(env['PATH_INFO'])
41
-
38
+ def serve_file(env)
39
+ @file_server.call(env)
40
+ end
41
+
42
+ def serve_rails(env)
42
43
  request = Request.new(env)
43
44
  response = Response.new
44
45
 
@@ -49,6 +50,20 @@ module Rack
49
50
 
50
51
  response.finish
51
52
  end
53
+
54
+ def call(env)
55
+ path = env['PATH_INFO'].chomp('/')
56
+ cached_path = (path.empty? ? 'index' : path) + ActionController::Base.page_cache_extension
57
+
58
+ if file_exist?(path) # Serve the file if it's there
59
+ serve_file(env)
60
+ elsif file_exist?(cached_path) # Serve the page cache if it's there
61
+ env['PATH_INFO'] = cached_path
62
+ serve_file(env)
63
+ else # No static file, let Rails handle it
64
+ serve_rails(env)
65
+ end
66
+ end
52
67
 
53
68
  protected
54
69
 
@@ -75,27 +90,28 @@ module Rack
75
90
  @response['Expires'] = options.delete('expires') if options['expires']
76
91
 
77
92
  @response.status = options.delete('Status') if options['Status']
78
-
79
- options.each { |k,v| @response[k] = v }
80
-
93
+
81
94
  # Convert 'cookie' header to 'Set-Cookie' headers.
82
- # According to http://www.faqs.org/rfcs/rfc2109.html:
83
- # the Set-Cookie response header comprises the token
84
- # Set-Cookie:, followed by a comma-separated list of
85
- # one or more cookies.
86
- if cookie = @response.header.delete('Cookie')
87
- cookies = case cookie
88
- when Array then cookie.collect { |c| c.to_s }.join(', ')
89
- when Hash then cookie.collect { |_, c| c.to_s }.join(', ')
90
- else cookie.to_s
95
+ # Because Set-Cookie header can appear more the once in the response body,
96
+ # we store it in a line break seperated string that will be translated to
97
+ # multiple Set-Cookie header by the handler.
98
+ if cookie = options.delete('cookie')
99
+ cookies = []
100
+
101
+ case cookie
102
+ when Array then cookie.each { |c| cookies << c.to_s }
103
+ when Hash then cookie.each { |_, c| cookies << c.to_s }
104
+ else cookies << cookie.to_s
91
105
  end
92
106
 
93
- cookies << ', ' + @output_cookies.each { |c| c.to_s }.join(', ') if @output_cookies
107
+ @output_cookies.each { |c| cookies << c.to_s } if @output_cookies
94
108
 
95
- @response['Set-Cookie'] = cookies
109
+ @response['Set-Cookie'] = [@response['Set-Cookie'], cookies].compact.join("\n")
96
110
  end
111
+
112
+ options.each { |k,v| @response[k] = v }
97
113
  end
98
-
114
+
99
115
  ""
100
116
  end
101
117
 
@@ -12,8 +12,9 @@ require 'thin/statuses'
12
12
 
13
13
  module Thin
14
14
  NAME = 'thin'.freeze
15
- SERVER = "#{NAME} #{VERSION::STRING}".freeze
15
+ SERVER = "#{NAME} #{VERSION::STRING} codename #{VERSION::CODENAME}".freeze
16
16
 
17
+ autoload :Cluster, 'thin/cluster'
17
18
  autoload :Connection, 'thin/connection'
18
19
  autoload :Daemonizable, 'thin/daemonizing'
19
20
  autoload :Logging, 'thin/logging'
@@ -0,0 +1,106 @@
1
+ module Thin
2
+ # Control a set of servers.
3
+ # * Generate start and stop commands and run them.
4
+ # * Inject the port number in the pid and log filenames.
5
+ # Servers are started throught the +thin+ commandline script.
6
+ class Cluster
7
+ include Logging
8
+
9
+ # Path to the +thin+ script used to control the servers.
10
+ # Leave this to default to use the one in the path.
11
+ attr_accessor :script
12
+
13
+ # Number of servers in the cluster.
14
+ attr_accessor :size
15
+
16
+ # Command line options passed to the thin script
17
+ attr_accessor :options
18
+
19
+ # Create a new cluster of servers launched using +options+.
20
+ def initialize(options)
21
+ @options = options.merge(:daemonize => true)
22
+ @size = @options.delete(:servers)
23
+ @script = 'thin'
24
+ end
25
+
26
+ def first_port; @options[:port] end
27
+ def address; @options[:address] end
28
+ def pid_file; File.expand_path File.join(@options[:chdir], @options[:pid]) end
29
+ def log_file; File.expand_path File.join(@options[:chdir], @options[:log]) end
30
+
31
+ # Start the servers
32
+ def start
33
+ with_each_server { |port| start_on_port port }
34
+ end
35
+
36
+ # Start the server on a single port
37
+ def start_on_port(port)
38
+ log "Starting #{address}:#{port} ... "
39
+
40
+ run :start, @options, port
41
+ end
42
+
43
+ # Stop the servers
44
+ def stop
45
+ with_each_server { |port| stop_on_port port }
46
+ end
47
+
48
+ # Stop the server running on +port+
49
+ def stop_on_port(port)
50
+ log "Stopping #{address}:#{port} ... "
51
+
52
+ run :stop, @options, port
53
+ end
54
+
55
+ # Stop and start the servers.
56
+ def restart
57
+ stop
58
+ sleep 0.1 # Let's breath a bit shall we ?
59
+ start
60
+ end
61
+
62
+ def log_file_for(port)
63
+ include_port_number log_file, port
64
+ end
65
+
66
+ def pid_file_for(port)
67
+ include_port_number pid_file, port
68
+ end
69
+
70
+ def pid_for(port)
71
+ File.read(pid_file_for(port)).chomp.to_i
72
+ end
73
+
74
+ private
75
+ # Send the command to the +thin+ script
76
+ def run(cmd, options, port)
77
+ shell_cmd = shellify(cmd, options.merge(:port => port, :pid => pid_file_for(port), :log => log_file_for(port)))
78
+ trace shell_cmd
79
+ ouput = `#{shell_cmd}`.chomp
80
+ log ouput unless ouput.empty?
81
+ end
82
+
83
+ # Turn into a runnable shell command
84
+ def shellify(cmd, options)
85
+ shellified_options = options.inject([]) do |args, (name, value)|
86
+ args << case value
87
+ when NilClass
88
+ when TrueClass then "--#{name}"
89
+ else "--#{name.to_s.tr('_', '-')}=#{value.inspect}"
90
+ end
91
+ end
92
+ "#{@script} #{cmd} #{shellified_options.compact.join(' ')}"
93
+ end
94
+
95
+ def with_each_server
96
+ @size.times { |n| yield first_port + n }
97
+ end
98
+
99
+ # Add the port numbers in the filename
100
+ # so each instance get its own file
101
+ def include_port_number(path, port)
102
+ raise ArgumentError, "filename '#{path}' must include an extension" unless path =~ /\./
103
+ path.gsub(/\.(.+)$/) { ".#{port}.#{$1}" }
104
+ end
105
+ end
106
+ end
@@ -12,11 +12,11 @@ module Thin
12
12
  end
13
13
 
14
14
  def receive_data(data)
15
+ trace { data }
15
16
  process if @request.parse(data)
16
17
  rescue InvalidRequest => e
17
18
  log "Invalid request"
18
19
  log_error e
19
- trace { data }
20
20
  close_connection
21
21
  end
22
22
 
@@ -30,9 +30,8 @@ module Thin
30
30
  @response.status, @response.headers, @response.body = @app.call(env)
31
31
 
32
32
  # Send the response
33
- send_data @response.head
34
- @response.body.rewind
35
- send_data @response.body.read
33
+ trace { @response.to_s }
34
+ send_data @response.to_s
36
35
 
37
36
  close_connection_after_writing
38
37
 
@@ -1,22 +1,5 @@
1
1
  require 'etc'
2
-
3
- module Kernel
4
- unless respond_to? :daemonize # Already part of Ruby 1.9, yeah!
5
- # Turns the current script into a daemon process that detaches from the console.
6
- # It can be shut down with a TERM signal. Taken from ActiveSupport.
7
- def daemonize
8
- exit if fork # Parent exits, child continues.
9
- Process.setsid # Become session leader.
10
- exit if fork # Zap session leader. See [1].
11
- Dir.chdir "/" # Release old working directory.
12
- File.umask 0000 # Ensure sensible umask. Adjust as needed.
13
- STDIN.reopen "/dev/null" # Free file descriptors and
14
- STDOUT.reopen "/dev/null", "a" # point them somewhere sensible.
15
- STDERR.reopen STDOUT # STDOUT/ERR should better go to a logfile.
16
- trap("TERM") { exit }
17
- end
18
- end
19
- end
2
+ require 'daemons'
20
3
 
21
4
  module Process
22
5
  # Returns +true+ the process identied by +pid+ is running.
@@ -51,13 +34,10 @@ module Thin
51
34
  raise ArgumentError, 'You must specify a pid_file to deamonize' unless @pid_file
52
35
 
53
36
  pwd = Dir.pwd # Current directory is changed during daemonization, so store it
54
- super # Calls Kernel#daemonize
55
- Dir.chdir pwd
56
37
 
57
- trap('HUP', 'IGNORE') # Don't die upon logout
58
-
59
- # Redirect output to the logfile
60
- [STDOUT, STDERR].each { |f| f.reopen @log_file, 'a' } if @log_file
38
+ Daemonize.daemonize(File.expand_path(@log_file))
39
+
40
+ Dir.chdir(pwd)
61
41
 
62
42
  write_pid_file
63
43
  at_exit do
@@ -5,6 +5,7 @@ module Thin
5
5
  # and the server can not process it.
6
6
  class InvalidRequest < IOError; end
7
7
 
8
+ # A request sent by the client to the server.
8
9
  class Request
9
10
  MAX_HEADER = 1024 * (80 + 32)
10
11
  MAX_HEADER_MSG = 'Header longer than allowed'.freeze
@@ -35,7 +36,7 @@ module Thin
35
36
  # Rack stuff
36
37
  RACK_INPUT => @body,
37
38
 
38
- RACK_VERSION => [0, 1],
39
+ RACK_VERSION => [0, 2],
39
40
  RACK_ERRORS => STDERR,
40
41
 
41
42
  RACK_MULTITHREAD => false,
@@ -47,18 +48,18 @@ module Thin
47
48
  def parse(data)
48
49
  @data << data
49
50
 
50
- if @parser.finished? # Header finished, can only be some more body
51
+ if @parser.finished? # Header finished, can only be some more body
51
52
  body << data
52
- elsif @data.size > MAX_HEADER
53
+ elsif @data.size > MAX_HEADER # Oho! very big header, must be a mean person
53
54
  raise InvalidRequest, MAX_HEADER_MSG
54
- else # Parse more header
55
+ else # Parse more header
55
56
  @nparsed = @parser.execute(@env, @data, @nparsed)
56
57
  end
57
58
 
58
59
  # Check if header and body are complete
59
60
  if @parser.finished? && body.size >= content_length
60
61
  body.rewind
61
- return true
62
+ return true # Request is fully parsed
62
63
  end
63
64
 
64
65
  false # Not finished, need more data
@@ -26,9 +26,7 @@ module Thin
26
26
 
27
27
  def headers=(key_value_pairs)
28
28
  key_value_pairs.each do |k, vs|
29
- vs.each do |v|
30
- @headers[k] = v
31
- end
29
+ vs.each { |v| @headers[k] = v.chomp }
32
30
  end
33
31
  end
34
32
 
@@ -4,9 +4,9 @@ module Thin
4
4
 
5
5
  # The Thin HTTP server used to served request.
6
6
  # It listen for incoming request on a given port
7
- # and forward all request to all the handlers in the order
8
- # they were registered.
9
- # Based on HTTP 1.1 protocol specs
7
+ # and forward all request to +app+.
8
+ #
9
+ # Based on HTTP 1.1 protocol specs:
10
10
  # http://www.w3.org/Protocols/rfc2616/rfc2616.html
11
11
  class Server
12
12
  include Logging
@@ -18,12 +18,12 @@ module Thin
18
18
  # App called with the request that produce the response.
19
19
  attr_accessor :app
20
20
 
21
- # Maximum time for a request to be red and parsed.
21
+ # Maximum time for incoming data to arrive
22
22
  attr_accessor :timeout
23
23
 
24
24
  # Creates a new server binded to <tt>host:port</tt>
25
25
  # that will pass request to +app+.
26
- def initialize(host, port, app)
26
+ def initialize(host, port, app=nil)
27
27
  @host = host
28
28
  @port = port.to_i
29
29
  @app = app
@@ -32,7 +32,9 @@ module Thin
32
32
 
33
33
  # Starts the handlers.
34
34
  def start
35
- log ">> Thin web server (v#{VERSION::STRING})"
35
+ raise ArgumentError, "app required" unless @app
36
+
37
+ log ">> Thin web server (v#{VERSION::STRING} codename #{VERSION::CODENAME})"
36
38
  trace ">> Tracing ON"
37
39
  end
38
40
 
@@ -64,6 +66,7 @@ module Thin
64
66
  end
65
67
  end
66
68
 
69
+
67
70
  def stop
68
71
  EventMachine.stop_event_loop
69
72
  rescue
@@ -1,9 +1,11 @@
1
1
  module Thin
2
2
  module VERSION #:nodoc:
3
- MAJOR = 0
4
- MINOR = 5
5
- TINY = 0
3
+ MAJOR = 0
4
+ MINOR = 5
5
+ TINY = 2
6
6
 
7
- STRING = [MAJOR, MINOR, TINY].join('.')
7
+ STRING = [MAJOR, MINOR, TINY].join('.')
8
+
9
+ CODENAME = 'Cheezburger'
8
10
  end
9
11
  end
Binary file
@@ -0,0 +1,58 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe Cluster do
4
+ before do
5
+ @cluster = Thin::Cluster.new(:chdir => File.dirname(__FILE__) + '/rails_app',
6
+ :address => '0.0.0.0',
7
+ :port => 3000,
8
+ :servers => 3,
9
+ :timeout => 10,
10
+ :log => 'thin.log',
11
+ :pid => 'thin.pid'
12
+ )
13
+ @cluster.script = File.dirname(__FILE__) + '/../bin/thin'
14
+ @cluster.silent = true
15
+ end
16
+
17
+ it 'should include port number in file names' do
18
+ @cluster.send(:include_port_number, 'thin.log', 3000).should == 'thin.3000.log'
19
+ @cluster.send(:include_port_number, 'thin.pid', 3000).should == 'thin.3000.pid'
20
+ proc { @cluster.send(:include_port_number, 'thin', 3000) }.should raise_error(ArgumentError)
21
+ end
22
+
23
+ it 'should call each server' do
24
+ calls = []
25
+ @cluster.send(:with_each_server) do |port|
26
+ calls << port
27
+ end
28
+ calls.should == [3000, 3001, 3002]
29
+ end
30
+
31
+ it 'should shellify command' do
32
+ out = @cluster.send(:shellify, :start, :port => 3000, :daemonize => true, :log => 'hi.log', :pid => nil)
33
+ out.should include('--port=3000', '--daemonize', '--log="hi.log"', 'thin start --')
34
+ out.should_not include('--pid=')
35
+ end
36
+
37
+ it 'should absolutize file path' do
38
+ @cluster.pid_file_for(3000).should == File.expand_path(File.dirname(__FILE__) + "/rails_app/thin.3000.pid")
39
+ end
40
+
41
+ it 'should start on specified port' do
42
+ @cluster.should_receive(:`) do |with|
43
+ with.should include('thin start', '--daemonize', 'thin.3001.log', 'thin.3001.pid', '--port=3001')
44
+ ''
45
+ end
46
+
47
+ @cluster.start_on_port 3001
48
+ end
49
+
50
+ it 'should stop on specified port' do
51
+ @cluster.should_receive(:`) do |with|
52
+ with.should include('thin stop', '--daemonize', 'thin.3001.log', 'thin.3001.pid', '--port=3001')
53
+ ''
54
+ end
55
+
56
+ @cluster.stop_on_port 3001
57
+ end
58
+ end