├── .github ├── FUNDING.yml └── workflows │ └── test.yml ├── .gitignore ├── CHANGELOG ├── Gemfile ├── README.md ├── Rakefile ├── benchmark ├── abc ├── benchmarker.rb └── runner ├── bin └── thin ├── example ├── adapter.rb ├── async_app.ru ├── async_chat.ru ├── async_tailer.ru ├── config.ru ├── monit_sockets ├── monit_unixsock ├── myapp.rb ├── ramaze.ru ├── thin.god ├── thin_solaris_smf.erb ├── thin_solaris_smf.readme.txt └── vlad.rake ├── ext └── thin_parser │ ├── common.rl │ ├── ext_help.h │ ├── extconf.rb │ ├── parser.c │ ├── parser.h │ ├── parser.rl │ └── thin.c ├── gems ├── rack-head.rb ├── rack-v1.rb ├── rack-v2.rb └── rack-v3.rb ├── lib ├── rack │ ├── adapter │ │ ├── loader.rb │ │ └── rails.rb │ └── handler │ │ └── thin.rb ├── rackup │ └── handler │ │ └── thin.rb ├── thin.rb └── thin │ ├── backends │ ├── base.rb │ ├── swiftiply_client.rb │ ├── tcp_server.rb │ └── unix_server.rb │ ├── command.rb │ ├── connection.rb │ ├── controllers │ ├── cluster.rb │ ├── controller.rb │ ├── service.rb │ └── service.sh.erb │ ├── daemonizing.rb │ ├── env.rb │ ├── headers.rb │ ├── logging.rb │ ├── rackup │ └── handler.rb │ ├── request.rb │ ├── response.rb │ ├── runner.rb │ ├── server.rb │ ├── stats.html.erb │ ├── stats.rb │ ├── statuses.rb │ └── version.rb ├── script ├── bleak ├── profile └── valgrind ├── site ├── images │ ├── bullet.gif │ ├── logo.gif │ ├── logo.psd │ └── split.gif ├── rdoc.rb ├── style.css └── thin.rb ├── spec ├── backends │ ├── swiftiply_client_spec.rb │ ├── tcp_server_spec.rb │ └── unix_server_spec.rb ├── command_spec.rb ├── configs │ ├── cluster.yml │ ├── single.yml │ └── with_erb.yml ├── connection_spec.rb ├── controllers │ ├── cluster_spec.rb │ ├── controller_spec.rb │ └── service_spec.rb ├── daemonizing_spec.rb ├── headers_spec.rb ├── logging_spec.rb ├── perf │ ├── request_perf_spec.rb │ ├── response_perf_spec.rb │ └── server_perf_spec.rb ├── rack │ ├── loader_spec.rb │ └── rails_adapter_spec.rb ├── rails_app │ ├── app │ │ ├── controllers │ │ │ ├── application.rb │ │ │ └── simple_controller.rb │ │ ├── helpers │ │ │ └── application_helper.rb │ │ └── views │ │ │ └── simple │ │ │ └── index.html.erb │ ├── config │ │ ├── boot.rb │ │ ├── environment.rb │ │ ├── environments │ │ │ ├── development.rb │ │ │ ├── production.rb │ │ │ └── test.rb │ │ ├── initializers │ │ │ ├── inflections.rb │ │ │ └── mime_types.rb │ │ └── routes.rb │ ├── public │ │ ├── .htaccess │ │ ├── 404.html │ │ ├── 422.html │ │ ├── 500.html │ │ ├── dispatch.cgi │ │ ├── dispatch.fcgi │ │ ├── dispatch.rb │ │ ├── favicon.ico │ │ ├── images │ │ │ └── rails.png │ │ ├── index.html │ │ ├── javascripts │ │ │ ├── application.js │ │ │ ├── controls.js │ │ │ ├── dragdrop.js │ │ │ ├── effects.js │ │ │ └── prototype.js │ │ └── robots.txt │ └── script │ │ ├── about │ │ ├── console │ │ ├── destroy │ │ ├── generate │ │ ├── performance │ │ ├── benchmarker │ │ ├── profiler │ │ └── request │ │ ├── plugin │ │ ├── process │ │ ├── inspector │ │ ├── reaper │ │ └── spawner │ │ ├── runner │ │ └── server ├── request │ ├── parser_spec.rb │ ├── persistent_spec.rb │ └── processing_spec.rb ├── response_spec.rb ├── runner_spec.rb ├── server │ ├── builder_spec.rb │ ├── robustness_spec.rb │ ├── stopping_spec.rb │ ├── swiftiply.yml │ ├── swiftiply_spec.rb │ ├── tcp_spec.rb │ ├── threaded_spec.rb │ └── unix_socket_spec.rb ├── server_spec.rb └── spec_helper.rb ├── tasks ├── announce.rake ├── deploy.rake ├── email.erb ├── ext.rake ├── rdoc.rake ├── site.rake ├── spec.rake └── stats.rake └── thin.gemspec /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | custom: https://github.com/socketry/community/#funding 3 | github: ioquatix 4 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | test: 16 | name: ${{matrix.ruby}} ${{matrix.gemfile}} 17 | runs-on: ${{matrix.os}}-latest 18 | continue-on-error: ${{matrix.experimental}} 19 | 20 | env: 21 | BUNDLE_GEMFILE: ${{matrix.gemfile}} 22 | 23 | strategy: 24 | matrix: 25 | experimental: [false] 26 | 27 | os: 28 | - ubuntu 29 | 30 | ruby: 31 | - 2.6 32 | - 2.7 33 | - "3.0" 34 | - 3.1 35 | - 3.2 36 | - 3.3 37 | - 3.4 38 | 39 | gemfile: 40 | - gems/rack-v1.rb 41 | - gems/rack-v2.rb 42 | - gems/rack-v3.rb 43 | 44 | include: 45 | - experimental: false 46 | os: macos 47 | ruby: 3.4 48 | gemfile: gems/rack-v2.rb 49 | - experimental: false 50 | os: macos 51 | ruby: 3.4 52 | gemfile: gems/rack-v3.rb 53 | - experimental: true 54 | os: ubuntu 55 | ruby: head 56 | gemfile: gems/rack-v2.rb 57 | - experimental: true 58 | os: ubuntu 59 | ruby: head 60 | gemfile: gems/rack-v3.rb 61 | - experimental: true 62 | os: ubuntu 63 | ruby: 3.4 64 | gemfile: gems/rack-head.rb 65 | exclude: 66 | - { ruby: 3.3, gemfile: gems/rack-v1.rb } 67 | - { ruby: 3.4, gemfile: gems/rack-v1.rb } 68 | 69 | steps: 70 | - uses: actions/checkout@v4 71 | 72 | - uses: ruby/setup-ruby@v1 73 | with: 74 | ruby-version: ${{matrix.ruby}} 75 | bundler-cache: true 76 | 77 | - name: Run tests 78 | timeout-minutes: 10 79 | run: bundle exec rake 80 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ext/thin_parser/Makefile 2 | ext/thin_parser/conftest.dSYM/* 3 | ext/thin_parser/*.log 4 | ext/thin_parser/*.o 5 | ext/thin_parser/*.bundle 6 | ext/thin_parser/*.obj 7 | ext/thin_parser/*mswin32* 8 | ext/thin_parser/vc60.pdb 9 | ext/thin_parser/*.so 10 | *.bundle 11 | *.so 12 | *.gem 13 | log 14 | spec/tmp 15 | spec/rails_app/log 16 | /vendor 17 | doc/rdoc/* 18 | tmp/* 19 | pkg/* 20 | nbproject/* 21 | .DS_Store 22 | daemon_test.log 23 | Gemfile.lock 24 | .idea -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | group :development do 6 | gem "rake-compiler" 7 | gem "rdoc" 8 | gem "benchmark" 9 | end 10 | 11 | group :test do 12 | gem "rake", ">= 12.3.3" 13 | gem "rspec", "~> 3.5" 14 | gem "ostruct" 15 | end 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Thin 2 | 3 | A small and fast Ruby web server 4 | 5 | ## Installation 6 | 7 | ``` 8 | gem install thin 9 | ``` 10 | 11 | Or add `thin` to your `Gemfile`: 12 | 13 | ```ruby 14 | gem 'thin' 15 | ``` 16 | 17 | ## Usage 18 | 19 | A +thin+ script offers an easy way to start your Rack application: 20 | 21 | ``` 22 | thin start 23 | ``` 24 | 25 | Browse the `example` directory for sample applications. 26 | 27 | ### CLI 28 | 29 | Use a rackup (config.ru) file and bind to localhost port 8080: 30 | 31 | ``` 32 | thin -R config.ru -a 127.0.0.1 -p 8080 start 33 | ``` 34 | 35 | Store the server process ID, log to a file and daemonize: 36 | 37 | ``` 38 | thin -p 9292 -P tmp/pids/thin.pid -l logs/thin.log -d start 39 | ``` 40 | 41 | Thin is quite flexible in that many options can be specified at the command line (see `thin -h` for more). 42 | 43 | ### Configuration files 44 | 45 | You can create a configuration file using `thin config -C config/thin.yml`. 46 | 47 | You can then use it with all commands, such as: `thin start -C config/thin.yml`. 48 | 49 | Here is an example config file: 50 | 51 | ```yaml 52 | --- 53 | user: www-data 54 | group: www-data 55 | pid: tmp/pids/thin.pid 56 | timeout: 30 57 | wait: 30 58 | log: log/thin.log 59 | max_conns: 1024 60 | require: [] 61 | environment: production 62 | max_persistent_conns: 512 63 | servers: 1 64 | threaded: true 65 | no-epoll: true 66 | daemonize: true 67 | socket: tmp/sockets/thin.sock 68 | chdir: /path/to/your/apps/root 69 | tag: a-name-to-show-up-in-ps aux 70 | ``` 71 | 72 | ## License 73 | 74 | Ruby License, http://www.ruby-lang.org/en/LICENSE.txt. 75 | 76 | ## Credits 77 | 78 | The parser was originally from Mongrel http://mongrel.rubyforge.org by Zed Shaw. 79 | Mongrel is copyright 2007 Zed A. Shaw and contributors. It is licensed under 80 | the Ruby license and the GPL2. 81 | 82 | Thin is copyright Marc-Andre Cournoyer 83 | 84 | Get help at http://groups.google.com/group/thin-ruby/ 85 | Report bugs at https://github.com/macournoyer/thin/issues 86 | and major security issues directly to me at macournoyer@gmail.com. 87 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rake/clean' 3 | load 'thin.gemspec' 4 | 5 | # Load tasks in tasks/ 6 | Dir['tasks/**/*.rake'].each { |rake| load rake } 7 | 8 | task :default => :spec 9 | 10 | desc "Build gem packages" 11 | task :build do 12 | sh "gem build thin.gemspec" 13 | end 14 | 15 | desc "Push gem packages" 16 | task :push => :build do 17 | sh "gem push thin-*.gem" 18 | end 19 | 20 | task :install => :build do 21 | sh "gem install thin-*.gem" 22 | end 23 | 24 | desc "Release version #{Thin::VERSION::STRING}" 25 | task :release => [:tag, :push] 26 | -------------------------------------------------------------------------------- /benchmark/abc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Automate benchmarking with ab with various concurrency levels. 3 | require 'optparse' 4 | 5 | options = { 6 | :address => '0.0.0.0', 7 | :port => 3000, 8 | :requests => 1000, 9 | :start => 1, 10 | :end => 100, 11 | :step => 10 12 | } 13 | 14 | OptionParser.new do |opts| 15 | opts.banner = "Usage: #{$PROGRAM_NAME} [options]" 16 | 17 | opts.on("-n", "--requests NUM", "Number of requests") { |num| options[:requests] = num } 18 | opts.on("-a", "--address HOST", "Address (default: 0.0.0.0)") { |host| options[:address] = host } 19 | opts.on("-p", "--port PORT", "use PORT (default: 3000)") { |port| options[:port] = port.to_i } 20 | opts.on("-s", "--start N", "First concurrency level") { |n| options[:start] = n.to_i } 21 | opts.on("-e", "--end N", "Last concurrency level") { |n| options[:end] = n.to_i } 22 | opts.on("-S", "--step N", "Concurrency level step") { |n| options[:step] = n.to_i } 23 | opts.on("-u", "--uri PATH", "Path to send to") { |u| options[:uri] = u } 24 | opts.on("-k", "--keep-alive", "Use Keep-Alive") { options[:keep_alive] = true } 25 | 26 | opts.on_tail("-h", "--help", "Show this message") { puts opts; exit } 27 | end.parse!(ARGV) 28 | 29 | puts 'request concurrency req/s failures' 30 | puts '=' * 42 31 | 32 | c = options[:start] 33 | until c >= options[:end] 34 | sleep 0.5 35 | out = `nice -n20 ab #{'-k' if options[:keep_alive]} -c #{c} -n #{options[:requests]} #{options[:address]}:#{options[:port]}/#{options[:uri]} 2> /dev/null` 36 | 37 | r = if requests = out.match(/^Requests.+?(\d+\.\d+)/) 38 | requests[1].to_i 39 | else 40 | 0 41 | end 42 | f = if requests = out.match(/^Failed requests.+?(\d+)/) 43 | requests[1].to_i 44 | else 45 | 0 46 | end 47 | 48 | puts "#{options[:requests].to_s.ljust(9)} #{c.to_s.ljust(13)} #{r.to_s.ljust(8)} #{f}" 49 | 50 | c += options[:step] 51 | end -------------------------------------------------------------------------------- /benchmark/benchmarker.rb: -------------------------------------------------------------------------------- 1 | require 'rack/lobster' 2 | 3 | class Benchmarker 4 | PORT = 7000 5 | ADDRESS = '0.0.0.0' 6 | 7 | attr_accessor :requests, :concurrencies, :servers, :keep_alive 8 | 9 | def initialize 10 | @servers = %w(Mongrel EMongrel Thin) 11 | @requests = 1000 12 | @concurrencies = [1, 10, 100] 13 | end 14 | 15 | def writer(&block) 16 | @writer = block 17 | end 18 | 19 | def run! 20 | @concurrencies.each do |concurrency| 21 | @servers.each do |server| 22 | req_sec, failed = run_one(server, concurrency) 23 | @writer.call(server, @requests, concurrency, req_sec, failed) 24 | end 25 | end 26 | end 27 | 28 | private 29 | def start_server(handler_name) 30 | @server = fork do 31 | [STDOUT, STDERR].each { |o| o.reopen "/dev/null" } 32 | 33 | case handler_name 34 | when 'EMongrel' 35 | require 'swiftcore/evented_mongrel' 36 | handler_name = 'Mongrel' 37 | end 38 | 39 | app = proc do |env| 40 | [200, {'content-type' => 'text/html', 'content-length' => '11'}, ['hello world']] 41 | end 42 | 43 | handler = Rack::Handler.const_get(handler_name) 44 | handler.run app, :Host => ADDRESS, :Port => PORT 45 | end 46 | 47 | sleep 2 48 | end 49 | 50 | def stop_server 51 | Process.kill('SIGKILL', @server) 52 | Process.wait 53 | end 54 | 55 | def run_ab(concurrency) 56 | `nice -n20 ab -c #{concurrency} -n #{@requests} #{@keep_alive ? '-k' : ''} #{ADDRESS}:#{PORT}/ 2> /dev/null` 57 | end 58 | 59 | def run_one(handler_name, concurrency) 60 | start_server(handler_name) 61 | 62 | out = run_ab(concurrency) 63 | 64 | stop_server 65 | 66 | req_sec = if matches = out.match(/^Requests.+?(\d+\.\d+)/) 67 | matches[1].to_i 68 | else 69 | 0 70 | end 71 | 72 | failed = if matches = out.match(/^Failed requests.+?(\d+)/) 73 | matches[1].to_i 74 | else 75 | 0 76 | end 77 | 78 | [req_sec, failed] 79 | end 80 | end -------------------------------------------------------------------------------- /benchmark/runner: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Simple benchmark to compare Thin performance against 3 | # other webservers supported by Rack. 4 | # 5 | # Run with: 6 | # 7 | # ruby simple.rb [num of request] [print|graph] [concurrency levels] 8 | # 9 | 10 | $: << File.join(File.dirname(__FILE__), '..', 'lib') 11 | require 'thin' 12 | 13 | require File.dirname(__FILE__) + '/benchmarker' 14 | require 'optparse' 15 | 16 | options = { 17 | :requests => 1000, 18 | :concurrencies => [1, 10, 100], 19 | :keep_alive => false, 20 | :output => :table 21 | } 22 | 23 | OptionParser.new do |opts| 24 | opts.banner = "Usage: #{$PROGRAM_NAME} [options]" 25 | 26 | opts.on("-n", "--requests NUM", "Number of requests") { |num| options[:requests] = num.to_i } 27 | opts.on("-c", "--concurrencies EXP", "Concurrency levels") { |exp| options[:concurrencies] = eval(exp).to_a } 28 | opts.on("-k", "--keep-alive", "Use persistent connections") { options[:keep_alive] = true } 29 | opts.on("-t", "--table", "Output as text table") { options[:output] = :table } 30 | opts.on("-g", "--graph", "Output as graph") { options[:output] = :graph } 31 | 32 | opts.on_tail("-h", "--help", "Show this message") { puts opts; exit } 33 | end.parse!(ARGV) 34 | 35 | # benchmark output_type, %w(WEBrick Mongrel EMongrel Thin), request, levels 36 | b = Benchmarker.new 37 | b.requests = options[:requests] 38 | b.concurrencies = options[:concurrencies] 39 | b.keep_alive = options[:keep_alive] 40 | 41 | case options[:output] 42 | when :table 43 | puts 'server request concurrency req/s failures' 44 | puts '=' * 52 45 | 46 | b.writer do |server, requests, concurrency, req_sec, failed| 47 | puts "#{server.ljust(8)} #{requests} #{concurrency.to_s.ljust(4)} #{req_sec.to_s.ljust(8)} #{failed}" 48 | end 49 | 50 | b.run! 51 | 52 | when :graph 53 | require '/usr/local/lib/ruby/gems/1.8/gems/gruff-0.2.9/lib/gruff' 54 | g = Gruff::Area.new 55 | g.title = "#{options[:requests]} requests" 56 | g.title << ' w/ Keep-Alive' if options[:keep_alive] 57 | 58 | g.x_axis_label = 'Concurrency' 59 | g.y_axis_label = 'Requests / sec' 60 | g.maximum_value = 0 61 | g.minimum_value = 0 62 | g.labels = {} 63 | b.concurrencies.each_with_index { |c, i| g.labels[i] = c.to_s } 64 | 65 | results = {} 66 | 67 | b.writer do |server, requests, concurrency, req_sec, failed| 68 | print '.' 69 | results[server] ||= [] 70 | results[server] << req_sec 71 | end 72 | 73 | b.run! 74 | puts 75 | 76 | results.each do |server, concurrencies| 77 | g.data(server, concurrencies) 78 | end 79 | 80 | g.write('bench.png') 81 | `open bench.png` 82 | end 83 | -------------------------------------------------------------------------------- /bin/thin: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Thin command line interface script. 3 | # Run thin -h to get more usage. 4 | 5 | require 'thin' 6 | Thin::Runner.new(ARGV).run! 7 | -------------------------------------------------------------------------------- /example/adapter.rb: -------------------------------------------------------------------------------- 1 | # Run with: ruby adapter.rb 2 | # Then browse to http://localhost:3000/test 3 | # and http://localhost:3000/files/adapter.rb 4 | require 'thin' 5 | 6 | class SimpleAdapter 7 | def call(env) 8 | body = ["hello!"] 9 | [ 10 | 200, 11 | { 'content-type' => 'text/plain' }, 12 | body 13 | ] 14 | end 15 | end 16 | 17 | Thin::Server.start('0.0.0.0', 3000) do 18 | use Rack::CommonLogger 19 | map '/test' do 20 | run SimpleAdapter.new 21 | end 22 | map '/files' do 23 | run Rack::Files.new('.') 24 | end 25 | end 26 | 27 | # You could also start the server like this: 28 | # 29 | # app = Rack::URLMap.new('/test' => SimpleAdapter.new, 30 | # '/files' => Rack::Files.new('.')) 31 | # Thin::Server.start('0.0.0.0', 3000, app) 32 | # 33 | -------------------------------------------------------------------------------- /example/async_app.ru: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rackup -s thin 2 | # 3 | # async_app.ru 4 | # raggi/thin 5 | # 6 | # A second demo app for async rack + thin app processing! 7 | # Now using http status code 100 instead. 8 | # 9 | # Created by James Tucker on 2008-06-17. 10 | # Copyright 2008 James Tucker . 11 | # 12 | #-- 13 | # Benchmark Results: 14 | # 15 | # raggi@mbk:~$ ab -c 100 -n 500 http://127.0.0.1:3000/ 16 | # This is ApacheBench, Version 2.0.40-dev <$Revision: 1.146 $> apache-2.0 17 | # Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ 18 | # Copyright 2006 The Apache Software Foundation, http://www.apache.org/ 19 | # 20 | # Benchmarking 127.0.0.1 (be patient) 21 | # Completed 100 requests 22 | # Completed 200 requests 23 | # Completed 300 requests 24 | # Completed 400 requests 25 | # Finished 500 requests 26 | # 27 | # 28 | # Server Software: thin 29 | # Server Hostname: 127.0.0.1 30 | # Server Port: 3000 31 | # 32 | # Document Path: / 33 | # Document Length: 12 bytes 34 | # 35 | # Concurrency Level: 100 36 | # Time taken for tests: 5.263089 seconds 37 | # Complete requests: 500 38 | # Failed requests: 0 39 | # Write errors: 0 40 | # Total transferred: 47000 bytes 41 | # HTML transferred: 6000 bytes 42 | # Requests per second: 95.00 [#/sec] (mean) 43 | # Time per request: 1052.618 [ms] (mean) 44 | # Time per request: 10.526 [ms] (mean, across all concurrent requests) 45 | # Transfer rate: 8.55 [Kbytes/sec] received 46 | # 47 | # Connection Times (ms) 48 | # min mean[+/-sd] median max 49 | # Connect: 0 3 2.2 3 8 50 | # Processing: 1042 1046 3.1 1046 1053 51 | # Waiting: 1037 1042 3.6 1041 1050 52 | # Total: 1045 1049 3.1 1049 1057 53 | # 54 | # Percentage of the requests served within a certain time (ms) 55 | # 50% 1049 56 | # 66% 1051 57 | # 75% 1053 58 | # 80% 1053 59 | # 90% 1054 60 | # 95% 1054 61 | # 98% 1056 62 | # 99% 1057 63 | # 100% 1057 (longest request) 64 | 65 | class DeferrableBody 66 | include EventMachine::Deferrable 67 | 68 | def call(body) 69 | body.each do |chunk| 70 | @body_callback.call(chunk) 71 | end 72 | end 73 | 74 | def each &blk 75 | @body_callback = blk 76 | end 77 | 78 | end 79 | 80 | class AsyncApp 81 | 82 | # This is a template async response. N.B. Can't use string for body on 1.9 83 | AsyncResponse = [-1, {}, []].freeze 84 | 85 | def call(env) 86 | 87 | body = DeferrableBody.new 88 | 89 | # Get the headers out there asap, let the client know we're alive... 90 | EventMachine::next_tick { env['async.callback'].call [200, {'content-type' => 'text/plain'}, body] } 91 | 92 | # Semi-emulate a long db request, instead of a timer, in reality we'd be 93 | # waiting for the response data. Whilst this happens, other connections 94 | # can be serviced. 95 | # This could be any callback based thing though, a deferrable waiting on 96 | # IO data, a db request, an http request, an smtp send, whatever. 97 | EventMachine::add_timer(1) { 98 | body.call ["Woah, async!\n"] 99 | 100 | EventMachine::next_tick { 101 | # This could actually happen any time, you could spawn off to new 102 | # threads, pause as a good looking lady walks by, whatever. 103 | # Just shows off how we can defer chunks of data in the body, you can 104 | # even call this many times. 105 | body.call ["Cheers then!"] 106 | body.succeed 107 | } 108 | } 109 | 110 | # throw :async # Still works for supporting non-async frameworks... 111 | 112 | AsyncResponse # May end up in Rack :-) 113 | end 114 | 115 | end 116 | 117 | # The additions to env for async.connection and async.callback absolutely 118 | # destroy the speed of the request if Lint is doing it's checks on env. 119 | # It is also important to note that an async response will not pass through 120 | # any further middleware, as the async response notification has been passed 121 | # right up to the webserver, and the callback goes directly there too. 122 | # Middleware could possibly catch :async, and also provide a different 123 | # async.connection and async.callback. 124 | 125 | # use Rack::Lint 126 | run AsyncApp.new 127 | -------------------------------------------------------------------------------- /example/async_tailer.ru: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rackup -s thin 2 | # 3 | # async_tailer.ru 4 | # raggi/thin 5 | # 6 | # Tested with 150 spawned tails on OS X 7 | # 8 | # Created by James Tucker on 2008-06-18. 9 | # Copyright 2008 James Tucker . 10 | 11 | # Uncomment if appropriate for you.. 12 | # EM.epoll 13 | # EM.kqueue 14 | 15 | class DeferrableBody 16 | include EventMachine::Deferrable 17 | 18 | def initialize 19 | @queue = [] 20 | # make sure to flush out the queue before closing the connection 21 | callback{ 22 | until @queue.empty? 23 | @queue.shift.each{|chunk| @body_callback.call(chunk) } 24 | end 25 | } 26 | end 27 | 28 | def schedule_dequeue 29 | return unless @body_callback 30 | EventMachine::next_tick do 31 | next unless body = @queue.shift 32 | body.each do |chunk| 33 | @body_callback.call(chunk) 34 | end 35 | schedule_dequeue unless @queue.empty? 36 | end 37 | end 38 | 39 | def call(body) 40 | @queue << body 41 | schedule_dequeue 42 | end 43 | 44 | def each &blk 45 | @body_callback = blk 46 | schedule_dequeue 47 | end 48 | 49 | end 50 | 51 | module TailRenderer 52 | attr_accessor :callback 53 | 54 | def receive_data(data) 55 | @callback.call([data]) 56 | end 57 | 58 | def unbind 59 | @callback.succeed 60 | end 61 | end 62 | 63 | class AsyncTailer 64 | 65 | AsyncResponse = [-1, {}, []].freeze 66 | 67 | def call(env) 68 | 69 | body = DeferrableBody.new 70 | 71 | EventMachine::next_tick do 72 | 73 | env['async.callback'].call [200, {'content-type' => 'text/html'}, body] 74 | 75 | body.call ["

Async Tailer

"]
 76 |       
 77 |     end
 78 |     
 79 |     EventMachine::popen('tail -f /var/log/system.log', TailRenderer) do |t|
 80 |       
 81 |       t.callback = body
 82 |       
 83 |       # If for some reason we 'complete' body, close the tail.
 84 |       body.callback do
 85 |         t.close_connection
 86 |       end
 87 |       
 88 |       # If for some reason the client disconnects, close the tail.
 89 |       body.errback do
 90 |         t.close_connection
 91 |       end
 92 |       
 93 |     end
 94 |     
 95 |     AsyncResponse
 96 |   end
 97 |   
 98 | end
 99 | 
100 | run AsyncTailer.new
101 | 


--------------------------------------------------------------------------------
/example/config.ru:
--------------------------------------------------------------------------------
 1 | # Run with: rackup -s thin
 2 | # then browse to http://localhost:9292
 3 | # Or with: thin start -R config.ru
 4 | # then browse to http://localhost:3000
 5 | # 
 6 | # Check Rack::Builder doc for more details on this file format:
 7 | #  http://rack.rubyforge.org/doc/classes/Rack/Builder.html
 8 | require 'thin'
 9 | 
10 | app = proc do |env|
11 |   # Response body has to respond to each and yield strings
12 |   # See Rack specs for more info: http://rack.rubyforge.org/doc/files/SPEC.html
13 |   body = ['hi!']
14 |   
15 |   [
16 |     200,                                        # Status code
17 |     { 'content-type' => 'text/html' },          # Reponse headers
18 |     body                                        # Body of the response
19 |   ]
20 | end
21 | 
22 | run app


--------------------------------------------------------------------------------
/example/monit_sockets:
--------------------------------------------------------------------------------
 1 | check process blog1
 2 |     with pidfile /u/apps/blog/shared/pids/thin.14000.pid
 3 |     start program = "ruby thin start -d -e production -u nobody -g nobody -p 14000 -a 127.0.0.1 -P tmp/pids/thin.14000.pid -c /u/apps/blog/current"
 4 |     stop program  = "ruby thin stop -P /u/apps/blog/shared/pids/thin.14000.pid"
 5 |     if totalmem > 90.0 MB for 5 cycles then restart
 6 |     if failed port 14000 then restart
 7 |     if cpu usage > 95% for 3 cycles then restart
 8 |     if 5 restarts within 5 cycles then timeout
 9 | 		group blog
10 | 
11 | check process blog2
12 |     with pidfile /u/apps/blog/shared/pids/thin.14001.pid
13 |     start program = "ruby thin start -d -e production -u nobody -g nobody -p 14001 -a 127.0.0.1 -P tmp/pids/thin.14001.pid -c /u/apps/blog/current"
14 |     stop program  = "ruby thin stop -P /u/apps/blog/shared/pids/thin.14001.pid"
15 |     if totalmem > 90.0 MB for 5 cycles then restart
16 |     if failed port 14001 then restart
17 |     if cpu usage > 95% for 3 cycles then restart
18 |     if 5 restarts within 5 cycles then timeout
19 | 		group blog
20 | 
21 | 


--------------------------------------------------------------------------------
/example/monit_unixsock:
--------------------------------------------------------------------------------
 1 | check process blog1
 2 |     with pidfile /u/apps/blog/shared/pids/thin.1.pid
 3 |     start program = "ruby thin start -d -e production -S /u/apps/blog/shared/pids/thin.1.sock -P tmp/pids/thin.1.pid -c /u/apps/blog/current"
 4 |     stop program  = "ruby thin stop -P /u/apps/blog/shared/pids/thin.1.pid"
 5 |     if totalmem > 90.0 MB for 5 cycles then restart
 6 | 		if failed unixsocket /u/apps/blog/shared/pids/thin.1.sock then restart
 7 |     if cpu usage > 95% for 3 cycles then restart
 8 |     if 5 restarts within 5 cycles then timeout
 9 | 		group blog
10 | 
11 | check process blog2
12 |     with pidfile /u/apps/blog/shared/pids/thin.2.pid
13 |     start program = "ruby thin start -d -e production -S /u/apps/blog/shared/pids/thin.2.sock -P tmp/pids/thin.2.pid -c /u/apps/blog/current"
14 |     stop program  = "ruby thin stop -P /u/apps/blog/shared/pids/thin.2.pid"
15 |     if totalmem > 90.0 MB for 5 cycles then restart
16 | 		if failed unixsocket /u/apps/blog/shared/pids/thin.2.sock then restart
17 |     if cpu usage > 95% for 3 cycles then restart
18 |     if 5 restarts within 5 cycles then timeout
19 | 		group blog
20 | 
21 | 


--------------------------------------------------------------------------------
/example/myapp.rb:
--------------------------------------------------------------------------------
1 | Myapp = lambda { |env| [200, {}, 'this is my app!'] }


--------------------------------------------------------------------------------
/example/ramaze.ru:
--------------------------------------------------------------------------------
 1 | # Ramaze Rackup config file.
 2 | # by tmm1
 3 | # Use with --rackup option:
 4 | # 
 5 | #   thin start -r ramaze.ru
 6 | # 
 7 | require 'start'
 8 | 
 9 | Ramaze.trait[:essentials].delete Ramaze::Adapter
10 | Ramaze.start :force => true
11 | 
12 | run Ramaze::Adapter::Base
13 | 


--------------------------------------------------------------------------------
/example/thin.god:
--------------------------------------------------------------------------------
 1 | # == God config file
 2 | # http://god.rubyforge.org/
 3 | # Authors: Gump and michael@glauche.de
 4 | #
 5 | # Config file for god that configures watches for each instance of a thin server for
 6 | # each thin configuration file found in /etc/thin.
 7 | # In order to get it working on Ubuntu, I had to make a change to god as noted at
 8 | # the following blog:
 9 | # http://blog.alexgirard.com/ruby-one-line-to-save-god/
10 | #
11 | require 'yaml'
12 | 
13 | config_path = "/etc/thin"
14 | 
15 | Dir[config_path + "/*.yml"].each do |file|
16 |   config = YAML.load_file(file)
17 |   num_servers = config["servers"] ||= 1
18 | 
19 |   (0...num_servers).each do |i|
20 |     # UNIX socket cluster use number 0 to 2 (for 3 servers)
21 |     # and tcp cluster use port number 3000 to 3002.
22 |     number = config['socket'] ? i : (config['port'] + i)
23 |     
24 |     God.watch do |w|
25 |       w.group = "thin-" + File.basename(file, ".yml")
26 |       w.name = w.group + "-#{number}"
27 |       
28 |       w.interval = 30.seconds
29 |       
30 |       w.uid = config["user"]
31 |       w.gid = config["group"]
32 |       
33 |       w.start = "thin start -C #{file} -o #{number}"
34 |       w.start_grace = 10.seconds
35 |       
36 |       w.stop = "thin stop -C #{file} -o #{number}"
37 |       w.stop_grace = 10.seconds
38 |       
39 |       w.restart = "thin restart -C #{file} -o #{number}"
40 | 
41 |       pid_path = config["chdir"] + "/" + config["pid"]
42 |       ext = File.extname(pid_path)
43 | 
44 |       w.pid_file = pid_path.gsub(/#{ext}$/, ".#{number}#{ext}")
45 |       
46 |       w.behavior(:clean_pid_file)
47 | 
48 |       w.start_if do |start|
49 |         start.condition(:process_running) do |c|
50 |           c.interval = 5.seconds
51 |           c.running = false
52 |         end
53 |       end
54 | 
55 |       w.restart_if do |restart|
56 |         restart.condition(:memory_usage) do |c|
57 |           c.above = 150.megabytes
58 |           c.times = [3,5] # 3 out of 5 intervals
59 |         end
60 | 
61 |         restart.condition(:cpu_usage) do |c|
62 |           c.above = 50.percent
63 |           c.times = 5
64 |         end
65 |       end
66 | 
67 |       w.lifecycle do |on|
68 |         on.condition(:flapping) do |c|
69 |           c.to_state = [:start, :restart]
70 |           c.times = 5
71 |           c.within = 5.minutes
72 |           c.transition = :unmonitored
73 |           c.retry_in = 10.minutes
74 |           c.retry_times = 5
75 |           c.retry_within = 2.hours
76 |         end
77 |       end
78 |     end
79 |   end
80 | end


--------------------------------------------------------------------------------
/example/thin_solaris_smf.erb:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 | 
 4 |   
 5 |     
 6 |     
 7 |       
 8 |     
 9 |     
10 |       
11 |       
12 |       
13 |     
14 |     <% 0.upto(thin_max_instances - 1) do |instance| %>
15 |     
16 |     
17 |       
18 |       
19 |         
20 |       
21 |       
24 |         
25 |           
26 |           
27 |             
28 |           
29 |         
30 |       
31 |       
32 |     
33 |     <% end %>
34 |   
35 | 
36 | 
37 | 


--------------------------------------------------------------------------------
/example/thin_solaris_smf.readme.txt:
--------------------------------------------------------------------------------
  1 | Using Thin with Solaris' SMF Monitoring Framework
  2 | - - - - - - - - - - - - - - - - - - - - - - - - -
  3 | 
  4 | Solaris uses the Service Management Framework (SMF) at the OS level to manage, monitor, and restart long running processes.  This replaces init scripts, and tools like monit and god.
  5 | 
  6 | The sample XML file (thin_solaris_smf.erb) is an example SMF manifest which I use on a Joyent accelerator which runs on OpenSolaris.
  7 | 
  8 | This setup will:
  9 | 
 10 | - ensure the right dependencies are loaded
 11 | - start n instances of Thin, and monitor each individually.  If any single one dies it will be restarted instantly (test it by killing a single thin instance and it will be back alive before you can type 'ps -ef').
 12 | 
 13 | This is better than using clustering since if you start the cluster with SMF it will only notice a problem and restart individual Thin's if ALL of them are dead, at which point it will restart the whole cluster.  This approach makes sure that all of your Thins start together and are monitored and managed independant of each other.  This problem likely exists if you are using god or monit to monitor only the start of the master cluster, and don't then monitor the individual processes started.
 14 | 
 15 | This example is in .erb format instead of plain XML since I dynamically generate this file as part of a Capistrano deployment.  In my deploy.rb file I define the variables found in this erb.  Of course you don't need to use this with Capistrano.  Just replace the few ERB variables from the xml file, change its extension, and load that directly in Solaris if you prefer.
 16 | 
 17 | Here are some examples for usage to get you started with Capistrano, and Thin:
 18 | 
 19 | FILE : config/deploy.rb
 20 | --
 21 | 
 22 |   require 'config/accelerator/accelerator_tasks'
 23 | 
 24 |   set :application, "yourapp"
 25 |   set :svcadm_bin, "/usr/sbin/svcadm"
 26 |   set :svccfg_bin, "/usr/sbin/svccfg"
 27 |   set :svcs_bin, "/usr/bin/svcs"
 28 | 
 29 |   # gets the list of remote service SMF names that we need to start
 30 |   # like (depending on thin_max_instances settings):
 31 |   # svc:/network/thin/yourapp-production:i_0
 32 |   # svc:/network/thin/yourapp-production:i_1
 33 |   # svc:/network/thin/yourapp-production:i_2
 34 |   set :service_list, "`svcs -H -o FMRI svc:network/thin/#{application}-production`"
 35 | 
 36 |   # how many Thin instances should be setup to run?
 37 |   # this affects the generated thin smf file, and the nginx vhost conf
 38 |   # need to re-run setup for thin smf and nginx vhost conf when changed
 39 |   set :thin_max_instances, 3
 40 | 
 41 |   # OVERRIDE STANDARD TASKS
 42 |   desc "Restart the entire application"
 43 |   deploy.task :restart do
 44 |     accelerator.thin.restart
 45 |     accelerator.nginx.restart
 46 |   end
 47 | 
 48 |   desc "Start the entire application"
 49 |   deploy.task :start do
 50 |     accelerator.thin.restart
 51 |     accelerator.nginx.restart
 52 |   end
 53 | 
 54 |   desc "Stop the entire application"
 55 |   deploy.task :stop do
 56 |     accelerator.thin.disable
 57 |     accelerator.nginx.disable
 58 |   end
 59 | 
 60 | 
 61 | FILE : config/accelerator/accelerator_tasks.rb
 62 | --
 63 | 
 64 |     desc "Create and deploy Thin SMF config"
 65 |     task :create_thin_smf, :roles => :app do
 66 |       service_name = application
 67 |       working_directory = current_path
 68 |       template = File.read("config/accelerator/thin_solaris_smf.erb")
 69 |       buffer = ERB.new(template).result(binding)
 70 |       put buffer, "#{shared_path}/#{application}-thin-smf.xml"
 71 |       sudo "#{svccfg_bin} import #{shared_path}/#{application}-thin-smf.xml"
 72 |     end
 73 | 
 74 |     desc "Delete Thin SMF config"
 75 |     task :delete_thin_smf, :roles => :app do
 76 |       accelerator.thin.disable
 77 |       sudo "#{svccfg_bin} delete /network/thin/#{application}-production"
 78 |     end
 79 | 
 80 |     desc "Show all SMF services"
 81 |     task :svcs do
 82 |       run "#{svcs_bin} -a" do |ch, st, data|
 83 |         puts data
 84 |       end
 85 |     end
 86 | 
 87 |     desc "Shows all non-functional SMF services"
 88 |     task :svcs_broken do
 89 |       run "#{svcs_bin} -vx" do |ch, st, data|
 90 |         puts data
 91 |       end
 92 |     end
 93 | 
 94 | 
 95 |     namespace :thin do
 96 | 
 97 |       desc "Disable all Thin servers"
 98 |       task :disable, :roles => :app do
 99 |         # temporarily disable, until next reboot (-t)
100 |         sudo "#{svcadm_bin} disable -t #{service_list}"
101 |       end
102 | 
103 |       desc "Enable all Thin servers"
104 |       task :enable, :roles => :app do
105 |         # start the app with all recursive dependencies
106 |         sudo "#{svcadm_bin} enable -r #{service_list}"
107 |       end
108 | 
109 |       desc "Restart all Thin servers"
110 |       task :restart, :roles => :app do
111 |         # svcadm restart doesn't seem to work right, so we'll brute force it
112 |         disable
113 |         enable
114 |       end
115 | 
116 |     end # namespace thin
117 | 
118 | 
119 | FILE : config/thin.yml
120 | --
121 | 
122 | ---
123 | pid: tmp/pids/thin.pid
124 | socket: /tmp/thin.sock
125 | log: log/thin.log
126 | max_conns: 1024
127 | timeout: 30
128 | chdir: /your/app/dir/rails/root
129 | environment: production
130 | max_persistent_conns: 512
131 | daemonize: true
132 | servers: 3
133 | 
134 | 
135 | FILE : config/accelerator/thin_solaris_smf.erb
136 | --
137 | This is of course an example.  It works for me, but YMMV
138 | 
139 | You may need to change this line to match your environment and config:
140 |   exec='/opt/csw/bin/thin -C config/thin.yml --only <%= instance.to_s %> start'
141 | 
142 | 
143 | CONTRIBUTE:
144 | 
145 | If you see problems or enhancements for this approach please send me an email at glenn [at] rempe dot us.  Sadly, I won't be able to provide support for this example as time and my limited Solaris admin skills won't allow.
146 | 
147 | Cheers,
148 | 
149 | Glenn Rempe
150 | 2008/03/20
151 | 


--------------------------------------------------------------------------------
/example/vlad.rake:
--------------------------------------------------------------------------------
 1 | # $GEM_HOME/gems/vlad-1.2.0/lib/vlad/thin.rb
 2 | # Thin tasks for Vlad the Deployer
 3 | # By cnantais
 4 | require 'vlad'
 5 | 
 6 | namespace :vlad do
 7 |   ##
 8 |   # Thin app server
 9 | 
10 |   set :thin_address,       nil
11 |   set :thin_command,       "thin"
12 |   set(:thin_conf)          { "#{shared_path}/thin_cluster.conf" }
13 |   set :thin_environment,   "production"
14 |   set :thin_group,         nil
15 |   set :thin_log_file,      nil
16 |   set :thin_pid_file,      nil
17 |   set :thin_port,          nil
18 |   set :thin_socket,        nil
19 |   set :thin_prefix,        nil
20 |   set :thin_servers,       2
21 |   set :thin_user,          nil
22 |   
23 |   set :thin_uses_bundler,  true
24 | 
25 |   desc "Prepares application servers for deployment. thin
26 | configuration is set via the thin_* variables.".cleanup
27 | 
28 |   remote_task :setup_app, :roles => :app do
29 |   
30 |     raise(ArgumentError, "Please provide either thin_socket or thin_port") if thin_port.nil? && thin_socket.nil?
31 |   
32 |     cmd = [
33 |            "config",
34 |            (%(-s "#{thin_servers}") if thin_servers),
35 |            (%(-S "#{thin_socket}") if thin_socket),
36 |            (%(-e "#{thin_environment}") if thin_environment),
37 |            (%(-a "#{thin_address}") if thin_address),
38 |            %(-c "#{current_path}"),
39 |            (%(-C "#{thin_conf}") if thin_conf),
40 |            (%(-P "#{thin_pid_file}") if thin_pid_file),
41 |            (%(-l "#{thin_log_file}") if thin_log_file),
42 |            (%(--user "#{thin_user}") if thin_user),
43 |            (%(--group "#{thin_group}") if thin_group),
44 |            (%(--prefix "#{thin_prefix}") if thin_prefix),
45 |            (%(-p "#{thin_port}") if thin_port),
46 |           ].compact.join ' '
47 | 
48 |     thin(cmd)
49 |   end
50 | 
51 |   def thin(cmd) # :nodoc:
52 |     command = if thin_uses_bundler
53 |       %(BUNDLE_GEMFILE="#{current_path}/Gemfile" bundle exec #{thin_command} #{cmd} -C "#{thin_conf}")
54 |     else
55 |       %(#{thin_command} #{cmd} -C "#{thin_conf}")
56 |     end
57 | 
58 |     %(cd "#{current_path}" && #{command})
59 |   end
60 | 
61 |   desc "Restart the app servers"
62 | 
63 |   remote_task :start_app, :roles => :app do
64 |     run thin(%(restart -O -s "#{thin_servers}"))
65 |   end
66 | 
67 |   desc "Stop the app servers"
68 | 
69 |   remote_task :stop_app, :roles => :app do
70 |     run thin(%(stop -s "#{thin_servers}"))
71 |   end
72 | end
73 | 


--------------------------------------------------------------------------------
/ext/thin_parser/common.rl:
--------------------------------------------------------------------------------
 1 | %%{
 2 |   
 3 |   machine http_parser_common;
 4 | 
 5 | #### HTTP PROTOCOL GRAMMAR
 6 | # line endings
 7 |   CRLF = "\r\n";
 8 | 
 9 | # character types
10 |   CTL = (cntrl | 127);
11 |   safe = ("$" | "-" | "_" | ".");
12 |   extra = ("!" | "*" | "'" | "(" | ")" | ",");
13 |   reserved = (";" | "/" | "?" | ":" | "@" | "&" | "=" | "+");
14 |   sorta_safe = ("\"" | "<" | ">");
15 |   unsafe = (CTL | " " | "#" | "%" | sorta_safe);
16 |   national = any -- (alpha | digit | reserved | extra | safe | unsafe);
17 |   unreserved = (alpha | digit | safe | extra | national);
18 |   escape = ("%" "u"? xdigit xdigit);
19 |   uchar = (unreserved | escape | sorta_safe);
20 |   pchar = (uchar | ":" | "@" | "&" | "=" | "+");
21 |   tspecials = ("(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\\" | "\"" | "/" | "[" | "]" | "?" | "=" | "{" | "}" | " " | "\t");
22 | 
23 | # elements
24 |   token = (ascii -- (CTL | tspecials));
25 | 
26 | # URI schemes and absolute paths
27 |   scheme = ( "http"i ("s"i)? );
28 |   hostname = ((alnum | "-" | "." | "_")+ | ("[" (":" | xdigit)+ "]"));
29 |   host_with_port = (hostname (":" digit*)?);
30 |   userinfo = ((unreserved | escape | ";" | ":" | "&" | "=" | "+")+ "@")*;
31 | 
32 |   path = ( pchar+ ( "/" pchar* )* ) ;
33 |   query = ( uchar | reserved )* %query_string ;
34 |   param = ( pchar | "/" )* ;
35 |   params = ( param ( ";" param )* ) ;
36 |   rel_path = (path? (";" params)? %request_path) ("?" %start_query query)?;
37 |   absolute_path = ( "/"+ rel_path );
38 |   path_uri = absolute_path > mark %request_uri;
39 |   Absolute_URI = (scheme "://" userinfo host_with_port path_uri);
40 | 
41 |   Request_URI = ((absolute_path | "*") >mark %request_uri) | Absolute_URI;
42 |   Fragment = ( uchar | reserved )* >mark %fragment;
43 |   Method = ( upper | digit | safe ){1,20} >mark %request_method;
44 | 
45 |   http_number = ( digit+ "." digit+ ) ;
46 |   HTTP_Version = ( "HTTP/" http_number ) >mark %request_http_version ;
47 |   Request_Line = ( Method " " Request_URI ("#" Fragment){0,1} " " HTTP_Version CRLF ) ;
48 | 
49 |   field_name = ( token -- ":" )+ >start_field %write_field;
50 | 
51 |   field_value = any* >start_value %write_value;
52 | 
53 |   message_header = field_name ":" " "* field_value :> CRLF;
54 | 
55 |   Request = Request_Line ( message_header )* ( CRLF @done );
56 | 
57 | main := Request;
58 | 
59 | }%%
60 | 


--------------------------------------------------------------------------------
/ext/thin_parser/ext_help.h:
--------------------------------------------------------------------------------
 1 | #ifndef ext_help_h
 2 | #define ext_help_h
 3 | 
 4 | #define RAISE_NOT_NULL(T) if(T == NULL) rb_raise(rb_eArgError, "NULL found for " # T " when shouldn't be.");
 5 | #define DATA_GET(from,type,name) Data_Get_Struct(from,type,name); RAISE_NOT_NULL(name);
 6 | #define REQUIRE_TYPE(V, T) if(TYPE(V) != T) rb_raise(rb_eTypeError, "Wrong argument type for " # V " required " # T);
 7 | 
 8 | #ifdef DEBUG
 9 | #define TRACE()  fprintf(stderr, "> %s:%d:%s\n", __FILE__, __LINE__, __FUNCTION__)
10 | #else
11 | #define TRACE() 
12 | #endif
13 | 
14 | #endif
15 | 


--------------------------------------------------------------------------------
/ext/thin_parser/extconf.rb:
--------------------------------------------------------------------------------
1 | require 'mkmf'
2 | 
3 | dir_config("thin_parser")
4 | have_library("c", "main")
5 | 
6 | create_makefile("thin_parser")
7 | 


--------------------------------------------------------------------------------
/ext/thin_parser/parser.h:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Copyright (c) 2005 Zed A. Shaw
 3 |  * You can redistribute it and/or modify it under the same terms as Ruby.
 4 |  */
 5 | 
 6 | #ifndef http11_parser_h
 7 | #define http11_parser_h
 8 | 
 9 | #include 
10 | 
11 | #if defined(_WIN32)
12 | #include 
13 | #endif
14 | 
15 | typedef void (*element_cb)(void *data, const char *at, size_t length);
16 | typedef void (*field_cb)(void *data, const char *field, size_t flen, const char *value, size_t vlen);
17 | 
18 | typedef struct http_parser { 
19 |   int cs;
20 |   size_t body_start;
21 |   int content_len;
22 |   size_t nread;
23 |   size_t mark;
24 |   size_t field_start;
25 |   size_t field_len;
26 |   size_t query_start;
27 | 
28 |   void *data;
29 | 
30 |   field_cb http_field;
31 |   element_cb request_method;
32 |   element_cb request_uri;
33 |   element_cb fragment;
34 |   element_cb request_path;
35 |   element_cb query_string;
36 |   element_cb request_http_version;
37 |   element_cb header_done;
38 |   
39 | } http_parser;
40 | 
41 | int thin_http_parser_init(http_parser *parser);
42 | int thin_http_parser_finish(http_parser *parser);
43 | size_t thin_http_parser_execute(http_parser *parser, const char *data, size_t len, size_t off);
44 | int thin_http_parser_has_error(http_parser *parser);
45 | int thin_http_parser_is_finished(http_parser *parser);
46 | 
47 | #define http_parser_nread(parser) (parser)->nread 
48 | 
49 | #endif
50 | 


--------------------------------------------------------------------------------
/ext/thin_parser/parser.rl:
--------------------------------------------------------------------------------
  1 | /**
  2 |  * Copyright (c) 2005 Zed A. Shaw
  3 |  * You can redistribute it and/or modify it under the same terms as Ruby.
  4 |  */
  5 | #include "parser.h"
  6 | #include 
  7 | #include 
  8 | #include 
  9 | #include 
 10 | #include 
 11 | 
 12 | #define LEN(AT, FPC) (FPC - buffer - parser->AT)
 13 | #define MARK(M,FPC) (parser->M = (FPC) - buffer)
 14 | #define PTR_TO(F) (buffer + parser->F)
 15 | 
 16 | /** Machine **/
 17 | 
 18 | %%{
 19 |   
 20 |   machine http_parser;
 21 | 
 22 |   action mark {MARK(mark, fpc); }
 23 | 
 24 | 
 25 |   action start_field { MARK(field_start, fpc); }
 26 |   action write_field { 
 27 |     parser->field_len = LEN(field_start, fpc);
 28 |   }
 29 | 
 30 |   action start_value { MARK(mark, fpc); }
 31 |   action write_value { 
 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 |     }
 35 |   }
 36 |   action request_method { 
 37 |     if (parser->request_method != NULL) {
 38 |       parser->request_method(parser->data, PTR_TO(mark), LEN(mark, fpc));
 39 |     }
 40 |   }
 41 |   action request_uri {
 42 |     if (parser->request_uri != NULL) {
 43 |       parser->request_uri(parser->data, PTR_TO(mark), LEN(mark, fpc));
 44 |     }
 45 |   }
 46 |   action fragment { 
 47 |     if (parser->fragment != NULL) {
 48 |       parser->fragment(parser->data, PTR_TO(mark), LEN(mark, fpc));
 49 |     }
 50 |   }
 51 | 
 52 |   action start_query {MARK(query_start, fpc); }
 53 |   action query_string { 
 54 |     if (parser->query_string != NULL) {
 55 |       parser->query_string(parser->data, PTR_TO(query_start), LEN(query_start, fpc));
 56 |     }
 57 |   }
 58 | 
 59 |   action request_http_version {
 60 |     if (parser->request_http_version != NULL) {
 61 |       parser->request_http_version(parser->data, PTR_TO(mark), LEN(mark, fpc));
 62 |     }
 63 |   }
 64 | 
 65 |   action request_path {
 66 |     if (parser->request_path != NULL) {
 67 |       parser->request_path(parser->data, PTR_TO(mark), LEN(mark,fpc));
 68 |     }
 69 |   }
 70 | 
 71 |   action done { 
 72 |     parser->body_start = fpc - buffer + 1; 
 73 |     if (parser->header_done != NULL) {
 74 |       parser->header_done(parser->data, fpc + 1, pe - fpc - 1);
 75 |     }
 76 |     fbreak;
 77 |   }
 78 | 
 79 |   include http_parser_common "common.rl";
 80 | 
 81 | }%%
 82 | 
 83 | /** Data **/
 84 | %% write data;
 85 | 
 86 | int thin_http_parser_init(http_parser *parser)  {
 87 |   int cs = 0;
 88 |   %% write init;
 89 |   parser->cs = cs;
 90 |   parser->body_start = 0;
 91 |   parser->content_len = 0;
 92 |   parser->mark = 0;
 93 |   parser->nread = 0;
 94 |   parser->field_len = 0;
 95 |   parser->field_start = 0;    
 96 | 
 97 |   return(1);
 98 | }
 99 | 
100 | 
101 | /** exec **/
102 | size_t thin_http_parser_execute(http_parser *parser, const char *buffer, size_t len, size_t off)  {
103 |   const char *p, *pe;
104 |   int cs = parser->cs;
105 | 
106 |   assert(off <= len && "offset past end of buffer");
107 | 
108 |   p = buffer+off;
109 |   pe = buffer+len;
110 | 
111 |   assert(*pe == '\0' && "pointer does not end on NUL");
112 |   assert(pe - p == (long)(len - off) && "pointers aren't same distance");
113 | 
114 | 
115 |   %% write exec;
116 | 
117 |   parser->cs = cs;
118 |   parser->nread += p - (buffer + off);
119 | 
120 |   assert(p <= pe && "buffer overflow after parsing execute");
121 |   assert(parser->nread <= len && "nread longer than length");
122 |   assert(parser->body_start <= len && "body starts after buffer end");
123 |   assert(parser->mark < len && "mark is after buffer end");
124 |   assert(parser->field_len <= len && "field has length longer than whole buffer");
125 |   assert(parser->field_start < len && "field starts after buffer end");
126 | 
127 |   if(parser->body_start) {
128 |     /* final \r\n combo encountered so stop right here */
129 |     parser->nread++;
130 |   }
131 | 
132 |   return(parser->nread);
133 | }
134 | 
135 | int thin_http_parser_has_error(http_parser *parser) {
136 |   return parser->cs == http_parser_error;
137 | }
138 | 
139 | int thin_http_parser_is_finished(http_parser *parser) {
140 |   return parser->cs == http_parser_first_final;
141 | }
142 | 
143 | int thin_http_parser_finish(http_parser *parser)
144 | {
145 |   if (thin_http_parser_has_error(parser) ) {
146 |     return -1;
147 |   } else if (thin_http_parser_is_finished(parser) ) {
148 |     return 1;
149 |   } else {
150 |     return 0;
151 |   }
152 | }
153 | 


--------------------------------------------------------------------------------
/gems/rack-head.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | 
3 | eval_gemfile "../Gemfile"
4 | 
5 | gem 'rack', github: 'rack/rack'
6 | gem 'rackup'
7 | 


--------------------------------------------------------------------------------
/gems/rack-v1.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | 
3 | eval_gemfile "../Gemfile"
4 | 
5 | gem 'rack', '~> 1.0'
6 | 


--------------------------------------------------------------------------------
/gems/rack-v2.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | 
3 | eval_gemfile "../Gemfile"
4 | 
5 | gem 'rack', "~> 2.0"
6 | 


--------------------------------------------------------------------------------
/gems/rack-v3.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 | 
3 | eval_gemfile "../Gemfile"
4 | 
5 | gem 'rack', "~> 3.0"
6 | gem 'rackup'
7 | 


--------------------------------------------------------------------------------
/lib/rack/adapter/loader.rb:
--------------------------------------------------------------------------------
 1 | module Rack
 2 |   class AdapterNotFound < RuntimeError; end
 3 |   
 4 |   # Mapping used to guess which adapter to use in Adapter.for.
 5 |   # Framework  =>  in order they will
 6 |   # be tested.
 7 |   # +nil+ for value to never guess.
 8 |   # NOTE: If a framework has a file that is not unique, make sure to place
 9 |   # it at the end.
10 |   ADAPTERS = [
11 |     [:rack,    'config.ru'],
12 |     [:rails,   'config/environment.rb'],
13 |     [:ramaze,  'start.rb'],
14 |     [:merb,    'config/init.rb'],
15 |     [:file,    nil]
16 |   ]
17 |   
18 |   # Rack v1 compatibility...
19 |   unless const_defined?(:Files)
20 |     require 'rack/file'
21 |     
22 |     Files = File
23 |   end
24 |   
25 |   module Adapter
26 |     # Guess which adapter to use based on the directory structure
27 |     # or file content.
28 |     # Returns a symbol representing the name of the adapter to use
29 |     # to load the application under dir/.
30 |     def self.guess(dir)
31 |       ADAPTERS.each do |adapter, file|
32 |         return adapter if file && ::File.exist?(::File.join(dir, file))
33 |       end
34 |       raise AdapterNotFound, "No adapter found for #{dir}"
35 |     end
36 |     
37 |     # Load a Rack application from a Rack config file (.ru).
38 |     def self.load(config)
39 |       rackup_code = ::File.read(config)
40 |       eval("Rack::Builder.new {( #{rackup_code}\n )}.to_app", TOPLEVEL_BINDING, config)
41 |     end
42 |     
43 |     # Loads an adapter identified by +name+ using +options+ hash.
44 |     def self.for(name, options={})
45 |       ENV['RACK_ENV'] = options[:environment]
46 |       
47 |       case name.to_sym
48 |       when :rack
49 |         return load(::File.join(options[:chdir], "config.ru"))
50 |         
51 |       when :rails
52 |         return Rails.new(options.merge(:root => options[:chdir]))
53 |       
54 |       when :ramaze
55 |         require "#{options[:chdir]}/start"
56 |         
57 |         Ramaze.trait[:essentials].delete Ramaze::Adapter
58 |         Ramaze.start :force => true
59 |         
60 |         return Ramaze::Adapter::Base
61 |         
62 |       when :merb
63 |         require 'merb-core'
64 |         
65 |         Merb::Config.setup(:merb_root   => options[:chdir],
66 |                            :environment => options[:environment])
67 |         Merb.environment = Merb::Config[:environment]
68 |         Merb.root = Merb::Config[:merb_root]
69 |         Merb::BootLoader.run
70 |         
71 |         return Merb::Rack::Application.new
72 |         
73 |       when :file
74 |         return Rack::Files.new(options[:chdir])
75 |         
76 |       else
77 |         raise AdapterNotFound, "Adapter not found: #{name}"
78 |         
79 |       end
80 |     end
81 |   end
82 | end


--------------------------------------------------------------------------------
/lib/rack/adapter/rails.rb:
--------------------------------------------------------------------------------
  1 | require 'cgi'
  2 | 
  3 | # Adapter to run a Rails app with any supported Rack handler.
  4 | # By default it will try to load the Rails application in the
  5 | # current directory in the development environment.
  6 | #
  7 | # Options:
  8 | #  root: Root directory of the Rails app
  9 | #  environment: Rails environment to run in (development [default], production or test)
 10 | #  prefix: Set the relative URL root.
 11 | #
 12 | # Based on http://fuzed.rubyforge.org/ Rails adapter
 13 | module Rack
 14 |   module Adapter
 15 |     class Rails
 16 |       FILE_METHODS = %w(GET HEAD).freeze
 17 | 
 18 |       def initialize(options = {})
 19 |         @root   = options[:root]        || Dir.pwd
 20 |         @env    = options[:environment] || 'development'
 21 |         @prefix = options[:prefix]
 22 | 
 23 |         load_application
 24 | 
 25 |         @rails_app = self.class.rack_based? ? ActionController::Dispatcher.new : CgiApp.new
 26 |         @file_app  = Rack::Files.new(::File.join(RAILS_ROOT, "public"))
 27 |       end
 28 | 
 29 |       def load_application
 30 |         ENV['RAILS_ENV'] = @env
 31 | 
 32 |         require "#{@root}/config/environment"
 33 |         require 'dispatcher'
 34 | 
 35 |         if @prefix
 36 |           if ActionController::Base.respond_to?(:relative_url_root=)
 37 |             ActionController::Base.relative_url_root = @prefix # Rails 2.1.1
 38 |           else
 39 |             ActionController::AbstractRequest.relative_url_root = @prefix
 40 |           end
 41 |         end
 42 |       end
 43 | 
 44 |       def file_exist?(path)
 45 |         full_path = ::File.join(@file_app.root, Utils.unescape(path))
 46 |         ::File.file?(full_path) && ::File.readable_real?(full_path)
 47 |       end
 48 | 
 49 |       def call(env)
 50 |         path        = env['PATH_INFO'].chomp('/')
 51 |         method      = env['REQUEST_METHOD']
 52 |         cached_path = (path.empty? ? 'index' : path) + ActionController::Base.page_cache_extension
 53 | 
 54 |         if FILE_METHODS.include?(method)
 55 |           if file_exist?(path)              # Serve the file if it's there
 56 |             return @file_app.call(env)
 57 |           elsif file_exist?(cached_path)    # Serve the page cache if it's there
 58 |             env['PATH_INFO'] = cached_path
 59 |             return @file_app.call(env)
 60 |           end
 61 |         end
 62 | 
 63 |         # No static file, let Rails handle it
 64 |         @rails_app.call(env)
 65 |       end
 66 | 
 67 |       def self.rack_based?
 68 |         rails_version = ::Rails::VERSION
 69 |         return false if rails_version::MAJOR < 2
 70 |         return false if rails_version::MAJOR == 2 && rails_version::MINOR < 2
 71 |         return false if rails_version::MAJOR == 2 && rails_version::MINOR == 2 && rails_version::TINY < 3
 72 |         true # >= 2.2.3
 73 |       end
 74 | 
 75 |       protected
 76 |         # For Rails pre Rack (2.3)
 77 |         class CgiApp
 78 |           def call(env)
 79 |             request         = Request.new(env)
 80 |             response        = Response.new
 81 |             session_options = ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS
 82 |             cgi             = CGIWrapper.new(request, response)
 83 | 
 84 |             Dispatcher.dispatch(cgi, session_options, response)
 85 | 
 86 |             response.finish
 87 |           end
 88 |         end
 89 | 
 90 |         class CGIWrapper < ::CGI
 91 |           def initialize(request, response, *args)
 92 |             @request  = request
 93 |             @response = response
 94 |             @args     = *args
 95 |             @input    = request.body
 96 | 
 97 |             super *args
 98 |           end
 99 | 
100 |           def header(options = 'text/html')
101 |             if options.is_a?(String)
102 |               @response['Content-Type']     = options unless @response['Content-Type']
103 |             else
104 |               @response['Content-Length']   = options.delete('Content-Length').to_s if options['Content-Length']
105 | 
106 |               @response['Content-Type']     = options.delete('type') || "text/html"
107 |               @response['Content-Type']    += '; charset=' + options.delete('charset') if options['charset']
108 | 
109 |               @response['Content-Language'] = options.delete('language') if options['language']
110 |               @response['Expires']          = options.delete('expires') if options['expires']
111 | 
112 |               @response.status              = options.delete('Status') if options['Status']
113 | 
114 |               # Convert 'cookie' header to 'Set-Cookie' headers.
115 |               # Because Set-Cookie header can appear more the once in the response body,
116 |               # we store it in a line break seperated string that will be translated to
117 |               # multiple Set-Cookie header by the handler.
118 |               if cookie = options.delete('cookie')
119 |                 cookies = []
120 | 
121 |                 case cookie
122 |                   when Array then cookie.each { |c| cookies << c.to_s }
123 |                   when Hash  then cookie.each { |_, c| cookies << c.to_s }
124 |                   else            cookies << cookie.to_s
125 |                 end
126 | 
127 |                 @output_cookies.each { |c| cookies << c.to_s } if @output_cookies
128 | 
129 |                 @response['Set-Cookie'] = [@response['Set-Cookie'], cookies].compact
130 |                 # See http://groups.google.com/group/rack-devel/browse_thread/thread/e8759b91a82c5a10/a8dbd4574fe97d69?#a8dbd4574fe97d69
131 |                 if Thin.ruby_18?
132 |                   @response['Set-Cookie'].flatten!
133 |                 else
134 |                   @response['Set-Cookie'] = @response['Set-Cookie'].join("\n")
135 |                 end
136 |               end
137 | 
138 |               options.each { |k, v| @response[k] = v }
139 |             end
140 | 
141 |             ''
142 |           end
143 | 
144 |           def params
145 |             @params ||= @request.params
146 |           end
147 | 
148 |           def cookies
149 |             @request.cookies
150 |           end
151 | 
152 |           def query_string
153 |             @request.query_string
154 |           end
155 | 
156 |           # Used to wrap the normal args variable used inside CGI.
157 |           def args
158 |             @args
159 |           end
160 | 
161 |           # Used to wrap the normal env_table variable used inside CGI.
162 |           def env_table
163 |             @request.env
164 |           end
165 | 
166 |           # Used to wrap the normal stdinput variable used inside CGI.
167 |           def stdinput
168 |             @input
169 |           end
170 | 
171 |           def stdoutput
172 |             STDERR.puts 'stdoutput should not be used.'
173 |             @response.body
174 |           end
175 |       end
176 |     end
177 |   end
178 | end
179 | 


--------------------------------------------------------------------------------
/lib/rack/handler/thin.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require 'rack/handler'
 4 | require_relative '../../thin/rackup/handler'
 5 | 
 6 | module Rack
 7 |   module Handler
 8 |     class Thin < ::Thin::Rackup::Handler
 9 |     end
10 | 
11 |     register :thin, Thin.to_s
12 |   end
13 | end
14 | 


--------------------------------------------------------------------------------
/lib/rackup/handler/thin.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require 'rackup/handler'
 4 | require_relative '../../thin/rackup/handler'
 5 | 
 6 | module Rackup
 7 |   module Handler
 8 |     class Thin < ::Thin::Rackup::Handler
 9 |     end
10 | 
11 |     register :thin, Thin
12 |   end
13 | end
14 | 


--------------------------------------------------------------------------------
/lib/thin.rb:
--------------------------------------------------------------------------------
 1 | require 'fileutils'
 2 | require 'timeout'
 3 | require 'stringio'
 4 | require 'time'
 5 | require 'forwardable'
 6 | require 'openssl'
 7 | require 'eventmachine'
 8 | require 'rack'
 9 | 
10 | module Thin
11 |   autoload :Command,            "thin/command"
12 |   autoload :Connection,         "thin/connection"
13 |   autoload :Daemonizable,       "thin/daemonizing"
14 |   autoload :Logging,            "thin/logging"
15 |   autoload :Headers,            "thin/headers"
16 |   autoload :Request,            "thin/request"
17 |   autoload :Response,           "thin/response"
18 |   autoload :Runner,             "thin/runner"
19 |   autoload :Server,             "thin/server"
20 |   autoload :Stats,              "thin/stats"
21 |   
22 |   module Backends
23 |     autoload :Base,             "thin/backends/base"
24 |     autoload :SwiftiplyClient,  "thin/backends/swiftiply_client"
25 |     autoload :TcpServer,        "thin/backends/tcp_server"
26 |     autoload :UnixServer,       "thin/backends/unix_server"
27 |   end
28 |   
29 |   module Controllers
30 |     autoload :Cluster,          "thin/controllers/cluster"
31 |     autoload :Controller,       "thin/controllers/controller"
32 |     autoload :Service,          "thin/controllers/service"
33 |   end
34 | end
35 | 
36 | require "thin/version"
37 | require "thin/statuses"
38 | require "rack/adapter/loader"
39 | require "thin_parser"
40 | 
41 | module Rack
42 |   module Adapter
43 |     autoload :Rails, "rack/adapter/rails"
44 |   end
45 | end
46 | 


--------------------------------------------------------------------------------
/lib/thin/backends/base.rb:
--------------------------------------------------------------------------------
  1 | module Thin
  2 |   module Backends
  3 |     # A Backend connects the server to the client. It handles:
  4 |     # * connection/disconnection to the server
  5 |     # * initialization of the connections
  6 |     # * monitoring of the active connections.
  7 |     #
  8 |     # == Implementing your own backend
  9 |     # You can create your own minimal backend by inheriting this class and
 10 |     # defining the +connect+ and +disconnect+ method.
 11 |     # If your backend is not based on EventMachine you also need to redefine
 12 |     # the +start+, +stop+, stop! and +config+ methods.
 13 |     class Base
 14 |       # Server serving the connections throught the backend
 15 |       attr_accessor :server
 16 |       
 17 |       # Maximum time for incoming data to arrive
 18 |       attr_accessor :timeout
 19 |       
 20 |       # Maximum number of file or socket descriptors that the server may open.
 21 |       attr_accessor :maximum_connections
 22 |       
 23 |       # Maximum number of connections that can be persistent
 24 |       attr_accessor :maximum_persistent_connections
 25 | 
 26 |       #allows setting of the eventmachine threadpool size
 27 |       attr_reader :threadpool_size
 28 |       def threadpool_size=(size)
 29 |         @threadpool_size = size
 30 |         EventMachine.threadpool_size = size
 31 |       end
 32 | 
 33 |       # Allow using threads in the backend.
 34 |       attr_writer :threaded
 35 |       def threaded?; @threaded end
 36 |             
 37 |       # Allow using SSL in the backend.
 38 |       attr_writer :ssl, :ssl_options
 39 |       def ssl?; @ssl end
 40 |       
 41 |       # Number of persistent connections currently opened
 42 |       attr_accessor :persistent_connection_count
 43 |       
 44 |       # Disable the use of epoll under Linux
 45 |       attr_accessor :no_epoll
 46 |       
 47 |       def initialize
 48 |         @connections                    = {}
 49 |         @timeout                        = Server::DEFAULT_TIMEOUT
 50 |         @persistent_connection_count    = 0
 51 |         @maximum_connections            = Server::DEFAULT_MAXIMUM_CONNECTIONS
 52 |         @maximum_persistent_connections = Server::DEFAULT_MAXIMUM_PERSISTENT_CONNECTIONS
 53 |         @no_epoll                       = false
 54 |         @running                        = false
 55 |         @ssl                            = nil
 56 |         @started_reactor                = false
 57 |         @stopping                       = false
 58 |         @threaded                       = nil
 59 |       end
 60 |       
 61 |       # Start the backend and connect it.
 62 |       def start
 63 |         @stopping = false
 64 |         starter   = proc do
 65 |           connect
 66 |           yield if block_given?
 67 |           @running = true
 68 |         end
 69 |         
 70 |         # Allow for early run up of eventmachine.
 71 |         if EventMachine.reactor_running?
 72 |           starter.call
 73 |         else
 74 |           @started_reactor = true
 75 |           EventMachine.run(&starter)
 76 |         end
 77 |       end
 78 |       
 79 |       # Stop of the backend from accepting new connections.
 80 |       def stop
 81 |         @running  = false
 82 |         @stopping = true
 83 |         
 84 |         # Do not accept anymore connection
 85 |         disconnect
 86 |         # Close idle persistent connections
 87 |         @connections.each_value { |connection| connection.close_connection if connection.idle? }
 88 |         stop! if @connections.empty?
 89 |       end
 90 |       
 91 |       # Force stop of the backend NOW, too bad for the current connections.
 92 |       def stop!
 93 |         @running  = false
 94 |         @stopping = false
 95 |         
 96 |         EventMachine.stop if @started_reactor && EventMachine.reactor_running?
 97 |         @connections.each_value { |connection| connection.close_connection }
 98 |         close
 99 |       end
100 |       
101 |       # Configure the backend. This method will be called before droping superuser privileges,
102 |       # so you can do crazy stuff that require godlike powers here.
103 |       def config
104 |         # See http://rubyeventmachine.com/pub/rdoc/files/EPOLL.html
105 |         EventMachine.epoll unless @no_epoll
106 |         
107 |         # Set the maximum number of socket descriptors that the server may open.
108 |         # The process needs to have required privilege to set it higher the 1024 on
109 |         # some systems.
110 |         @maximum_connections = EventMachine.set_descriptor_table_size(@maximum_connections) unless Thin.win?
111 |       end
112 |       
113 |       # Free up resources used by the backend.
114 |       def close
115 |       end
116 |       
117 |       # Returns +true+ if the backend is connected and running.
118 |       def running?
119 |         @running
120 |       end
121 | 
122 |       def started_reactor?
123 |         @started_reactor
124 |       end
125 |             
126 |       # Called by a connection when it's unbinded.
127 |       def connection_finished(connection)
128 |         @persistent_connection_count -= 1 if connection.can_persist?
129 |         @connections.delete(connection.__id__)
130 |         
131 |         # Finalize gracefull stop if there's no more active connection.
132 |         stop! if @stopping && @connections.empty?
133 |       end
134 |       
135 |       # Returns +true+ if no active connection.
136 |       def empty?
137 |         @connections.empty?
138 |       end
139 |       
140 |       # Number of active connections.
141 |       def size
142 |         @connections.size
143 |       end
144 |       
145 |       protected
146 |         # Initialize a new connection to a client.
147 |         def initialize_connection(connection)
148 |           connection.backend                 = self
149 |           connection.app                     = @server.app
150 |           connection.comm_inactivity_timeout = @timeout
151 |           connection.threaded                = @threaded
152 |           
153 |           if @ssl
154 |             connection.start_tls(@ssl_options)
155 |           end
156 | 
157 |           # We control the number of persistent connections by keeping
158 |           # a count of the total one allowed yet.
159 |           if @persistent_connection_count < @maximum_persistent_connections
160 |             connection.can_persist!
161 |             @persistent_connection_count += 1
162 |           end
163 | 
164 |           @connections[connection.__id__] = connection
165 |         end
166 |       
167 |     end
168 |   end
169 | end
170 | 


--------------------------------------------------------------------------------
/lib/thin/backends/swiftiply_client.rb:
--------------------------------------------------------------------------------
 1 | module Thin
 2 |   module Backends
 3 |     # Backend to act as a Swiftiply client (http://swiftiply.swiftcore.org).
 4 |     class SwiftiplyClient < Base
 5 |       attr_accessor :key
 6 | 
 7 |       attr_accessor :host, :port
 8 | 
 9 |       def initialize(host, port, options = {})
10 |         @host = host
11 |         @port = port.to_i
12 |         @key  = options[:swiftiply].to_s
13 |         super()
14 |       end
15 | 
16 |       # Connect the server
17 |       def connect
18 |         EventMachine.connect(@host, @port, SwiftiplyConnection, &method(:initialize_connection))
19 |       end
20 | 
21 |       # Stops the server
22 |       def disconnect
23 |         EventMachine.stop
24 |       end
25 | 
26 |       def to_s
27 |         "#{@host}:#{@port} swiftiply"
28 |       end
29 |     end
30 |   end
31 | 
32 |   class SwiftiplyConnection < Connection
33 |     def connection_completed
34 |       send_data swiftiply_handshake(@backend.key)
35 |     end
36 | 
37 |     def persistent?
38 |       true
39 |     end
40 | 
41 |     def unbind
42 |       super
43 |       EventMachine.add_timer(rand(2)) { reconnect(@backend.host, @backend.port) } if @backend.running?
44 |     end
45 | 
46 |     protected
47 |       def swiftiply_handshake(key)
48 |         'swiftclient' << host_ip.collect { |x| sprintf('%02x', x.to_i) }.join << sprintf('%04x', @backend.port) << sprintf('%02x', key.length) << key
49 |       end
50 | 
51 |       # For some reason Swiftiply request the current host
52 |       def host_ip
53 |         begin
54 |           if defined?(Addrinfo)
55 |             # ruby 2.0+
56 |             # TODO: ipv6 support here?
57 |             Addrinfo.getaddrinfo(@backend.host, @backend.port, :PF_INET, :STREAM).first.ip_address.split('.').map(&:to_i)
58 |           else
59 |             Socket.gethostbyname(@backend.host)[3].unpack('CCCC')
60 |           end
61 |         rescue
62 |           [0, 0, 0, 0]
63 |         end
64 |       end
65 |   end
66 | end


--------------------------------------------------------------------------------
/lib/thin/backends/tcp_server.rb:
--------------------------------------------------------------------------------
 1 | module Thin
 2 |   module Backends
 3 |     # Backend to act as a TCP socket server.
 4 |     class TcpServer < Base
 5 |       # Address and port on which the server is listening for connections.
 6 |       attr_accessor :host, :port
 7 | 
 8 |       def initialize(host, port)
 9 |         @host = host
10 |         @port = port
11 |         super()
12 |       end
13 | 
14 |       # Connect the server
15 |       def connect
16 |         @signature = EventMachine.start_server(@host, @port, Connection, &method(:initialize_connection))
17 |         binary_name = EventMachine.get_sockname( @signature )
18 |         port_name = Socket.unpack_sockaddr_in( binary_name )
19 |         @port = port_name[0]
20 |         @host = port_name[1]
21 |         @signature
22 |       end
23 | 
24 |       # Stops the server
25 |       def disconnect
26 |         EventMachine.stop_server(@signature)
27 |       end
28 | 
29 |       def to_s
30 |         "#{@host}:#{@port}"
31 |       end
32 |     end
33 |   end
34 | end
35 | 


--------------------------------------------------------------------------------
/lib/thin/backends/unix_server.rb:
--------------------------------------------------------------------------------
 1 | module Thin
 2 |   module Backends
 3 |     # Backend to act as a UNIX domain socket server.
 4 |     class UnixServer < Base
 5 |       # UNIX domain socket on which the server is listening for connections.
 6 |       attr_accessor :socket
 7 |       
 8 |       def initialize(socket)
 9 |         raise PlatformNotSupported, 'UNIX domain sockets not available on Windows' if Thin.win?
10 |         @socket = socket
11 |         super()
12 |       end
13 |       
14 |       # Connect the server
15 |       def connect
16 |         at_exit { remove_socket_file } # In case it crashes
17 |         old_umask = File.umask(0)
18 |         begin
19 |           EventMachine.start_unix_domain_server(@socket, UnixConnection, &method(:initialize_connection))
20 |           # HACK EventMachine.start_unix_domain_server doesn't return the connection signature
21 |           #      so we have to go in the internal stuff to find it.
22 |         @signature = EventMachine.instance_eval{@acceptors.keys.first}
23 |         ensure
24 |           File.umask(old_umask)
25 |         end
26 |       end
27 |       
28 |       # Stops the server
29 |       def disconnect
30 |         EventMachine.stop_server(@signature)
31 |       end
32 |       
33 |       # Free up resources used by the backend.
34 |       def close
35 |         remove_socket_file
36 |       end
37 |       
38 |       def to_s
39 |         @socket
40 |       end
41 |       
42 |       protected
43 |         def remove_socket_file
44 |           File.delete(@socket) if @socket && File.exist?(@socket)
45 |         end
46 |     end    
47 |   end
48 | 
49 |   # Connection through a UNIX domain socket.
50 |   class UnixConnection < Connection
51 |     protected
52 |       def socket_address        
53 |         '127.0.0.1' # Unix domain sockets can only be local
54 |       end
55 |   end
56 | end
57 | 


--------------------------------------------------------------------------------
/lib/thin/command.rb:
--------------------------------------------------------------------------------
 1 | require 'open3'
 2 | 
 3 | module Thin
 4 |   # Run a command through the +thin+ command-line script.
 5 |   class Command
 6 |     include Logging
 7 |     
 8 |     class << self
 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 |     end
13 |     
14 |     def initialize(name, options={})
15 |       @name    = name
16 |       @options = options
17 |     end
18 |     
19 |     def self.run(*args)
20 |       new(*args).run
21 |     end
22 |     
23 |     # Send the command to the +thin+ script
24 |     def run
25 |       shell_cmd = shellify
26 |       trace shell_cmd
27 |       trap('INT') {} # Ignore INT signal to pass CTRL+C to subprocess
28 |       Open3.popen3(shell_cmd) do |stdin, stdout, stderr|
29 |         log_info stdout.gets until stdout.eof?
30 |         log_info stderr.gets until stderr.eof?
31 |       end
32 |     end
33 |     
34 |     # Turn into a runnable shell command
35 |     def shellify
36 |       shellified_options = @options.inject([]) do |args, (name, value)|
37 |         option_name = name.to_s.tr("_", "-")
38 |         case value
39 |         when NilClass,
40 |              TrueClass then args << "--#{option_name}"
41 |         when FalseClass
42 |         when Array     then value.each { |v| args << "--#{option_name}=#{v.inspect}" }
43 |         else                args << "--#{option_name}=#{value.inspect}"
44 |         end
45 |         args
46 |       end
47 |       
48 |       raise ArgumentError, "Path to thin script can't be found, set Command.script" unless self.class.script
49 |       
50 |       "#{self.class.script} #{@name} #{shellified_options.compact.join(' ')}"
51 |     end
52 |   end
53 | end
54 | 


--------------------------------------------------------------------------------
/lib/thin/controllers/cluster.rb:
--------------------------------------------------------------------------------
  1 | require 'socket'
  2 | 
  3 | module Thin
  4 |   # An exception class to handle the event that server didn't start on time
  5 |   class RestartTimeout < RuntimeError; end
  6 |   
  7 |   module Controllers
  8 |     # Control a set of servers.
  9 |     # * Generate start and stop commands and run them.
 10 |     # * Inject the port or socket number in the pid and log filenames.
 11 |     # Servers are started throught the +thin+ command-line script.
 12 |     class Cluster < Controller
 13 |       # Cluster only options that should not be passed in the command sent
 14 |       # to the indiviual servers.
 15 |       CLUSTER_OPTIONS = [:servers, :only, :onebyone, :wait]
 16 |       
 17 |       # Maximum wait time for the server to be restarted
 18 |       DEFAULT_WAIT_TIME = 30    # seconds
 19 |       
 20 |       # Create a new cluster of servers launched using +options+.
 21 |       def initialize(options)
 22 |         super
 23 |         # Cluster can only contain daemonized servers
 24 |         @options.merge!(:daemonize => true)
 25 |       end
 26 |       
 27 |       def first_port; @options[:port]     end
 28 |       def address;    @options[:address]  end
 29 |       def socket;     @options[:socket]   end
 30 |       def pid_file;   @options[:pid]      end
 31 |       def log_file;   @options[:log]      end
 32 |       def size;       @options[:servers]  end
 33 |       def only;       @options[:only]     end
 34 |       def onebyone;   @options[:onebyone] end
 35 |       def wait;       @options[:wait]     end
 36 |       
 37 |       def swiftiply?
 38 |         @options.has_key?(:swiftiply)
 39 |       end
 40 |     
 41 |       # Start the servers
 42 |       def start
 43 |         with_each_server { |n| start_server n }
 44 |       end
 45 |     
 46 |       # Start a single server
 47 |       def start_server(number)
 48 |         log_info "Starting server on #{server_id(number)} ... "
 49 |       
 50 |         run :start, number
 51 |       end
 52 |   
 53 |       # Stop the servers
 54 |       def stop
 55 |         with_each_server { |n| stop_server n }
 56 |       end
 57 |     
 58 |       # Stop a single server
 59 |       def stop_server(number)
 60 |         log_info "Stopping server on #{server_id(number)} ... "
 61 |       
 62 |         run :stop, number
 63 |       end
 64 |     
 65 |       # Stop and start the servers.
 66 |       def restart
 67 |         unless onebyone
 68 |           # Let's do a normal restart by defaults
 69 |           stop
 70 |           sleep 0.1 # Let's breath a bit shall we ?
 71 |           start
 72 |         else
 73 |           with_each_server do |n| 
 74 |             stop_server(n)
 75 |             sleep 0.1 # Let's breath a bit shall we ?
 76 |             start_server(n)
 77 |             wait_until_server_started(n)
 78 |           end
 79 |         end
 80 |       end
 81 |       
 82 |       def test_socket(number)
 83 |         if socket
 84 |           UNIXSocket.new(socket_for(number))
 85 |         else
 86 |           TCPSocket.new(address, number)
 87 |         end
 88 |       rescue
 89 |         nil
 90 |       end
 91 |       
 92 |       # Make sure the server is running before moving on to the next one.
 93 |       def wait_until_server_started(number)
 94 |         log_info "Waiting for server to start ..."
 95 |         STDOUT.flush # Need this to make sure user got the message
 96 |         
 97 |         tries = 0
 98 |         loop do
 99 |           if test_socket = test_socket(number)
100 |             test_socket.close
101 |             break
102 |           elsif tries < wait
103 |             sleep 1
104 |             tries += 1
105 |           else
106 |             raise RestartTimeout, "The server didn't start in time. Please look at server's log file " +
107 |                                   "for more information, or set the value of 'wait' in your config " +
108 |                                   "file to be higher (defaults: 30)."
109 |           end
110 |         end
111 |       end
112 |     
113 |       def server_id(number)
114 |         if socket
115 |           socket_for(number)
116 |         elsif swiftiply?
117 |           [address, first_port, number].join(':')
118 |         else
119 |           [address, number].join(':')
120 |         end
121 |       end
122 |     
123 |       def log_file_for(number)
124 |         include_server_number log_file, number
125 |       end
126 |     
127 |       def pid_file_for(number)
128 |         include_server_number pid_file, number
129 |       end
130 |     
131 |       def socket_for(number)
132 |         include_server_number socket, number
133 |       end
134 |     
135 |       def pid_for(number)
136 |         File.read(pid_file_for(number)).chomp.to_i
137 |       end
138 |       
139 |       private
140 |         # Send the command to the +thin+ script
141 |         def run(cmd, number)
142 |           cmd_options = @options.reject { |option, value| CLUSTER_OPTIONS.include?(option) }
143 |           cmd_options.merge!(:pid => pid_file_for(number), :log => log_file_for(number))
144 |           if socket
145 |             cmd_options.merge!(:socket => socket_for(number))
146 |           elsif swiftiply?
147 |             cmd_options.merge!(:port => first_port)
148 |           else
149 |             cmd_options.merge!(:port => number)
150 |           end
151 |           Command.run(cmd, cmd_options)
152 |         end
153 |       
154 |         def with_each_server
155 |           if only
156 |             if first_port && only < 80
157 |               # interpret +only+ as a sequence number
158 |               yield first_port + only
159 |             else
160 |               # interpret +only+ as an absolute port number
161 |               yield only
162 |             end
163 |           elsif socket || swiftiply?
164 |             size.times { |n| yield n }
165 |           else
166 |             size.times { |n| yield first_port + n }
167 |           end
168 |         end
169 |       
170 |         # Add the server port or number in the filename
171 |         # so each instance get its own file
172 |         def include_server_number(path, number)
173 |           ext = File.extname(path)
174 |           path.gsub(/#{ext}$/, ".#{number}#{ext}")
175 |         end
176 |     end
177 |   end
178 | end
179 | 


--------------------------------------------------------------------------------
/lib/thin/controllers/service.rb:
--------------------------------------------------------------------------------
 1 | require 'erb'
 2 | 
 3 | module Thin
 4 |   module Controllers
 5 |     # System service controller to launch all servers which
 6 |     # config files are in a directory.
 7 |     class Service < Controller
 8 |       INITD_PATH          = File.directory?('/etc/rc.d') ? '/etc/rc.d/thin' : '/etc/init.d/thin'
 9 |       DEFAULT_CONFIG_PATH = '/etc/thin'
10 |       TEMPLATE            = File.dirname(__FILE__) + '/service.sh.erb'
11 |     
12 |       def initialize(options)
13 |         super
14 |       
15 |         raise PlatformNotSupported, 'Running as a service only supported on Linux' unless Thin.linux?
16 |       end
17 |     
18 |       def config_path
19 |         @options[:all] || DEFAULT_CONFIG_PATH
20 |       end
21 |     
22 |       def start
23 |         run :start
24 |       end
25 |     
26 |       def stop
27 |         run :stop
28 |       end
29 |     
30 |       def restart
31 |         run :restart
32 |       end
33 |     
34 |       def install(config_files_path=DEFAULT_CONFIG_PATH)
35 |         if File.exist?(INITD_PATH)
36 |           log_info "Thin service already installed at #{INITD_PATH}"
37 |         else
38 |           log_info "Installing thin service at #{INITD_PATH} ..."
39 |           sh "mkdir -p #{File.dirname(INITD_PATH)}"
40 |           log_info "writing #{INITD_PATH}"        
41 |           File.open(INITD_PATH, 'w') do |f|
42 |             f << ERB.new(File.read(TEMPLATE)).result(binding)
43 |           end
44 |           sh "chmod +x #{INITD_PATH}" # Make executable
45 |         end
46 |       
47 |         sh "mkdir -p #{config_files_path}"
48 | 
49 |         log_info ''
50 |         log_info "To configure thin to start at system boot:"
51 |         log_info "on RedHat like systems:"
52 |         log_info "  sudo /sbin/chkconfig --level 345 #{NAME} on"
53 |         log_info "on Debian-like systems (Ubuntu):"
54 |         log_info "  sudo /usr/sbin/update-rc.d -f #{NAME} defaults"
55 |         log_info "on Gentoo:"
56 |         log_info "  sudo rc-update add #{NAME} default"
57 |         log_info ''
58 |         log_info "Then put your config files in #{config_files_path}"
59 |       end
60 |     
61 |       private
62 |         def run(command)
63 |           Dir[config_path + '/*'].each do |config|
64 |             next if config.end_with?("~")
65 |             log_info "[#{command}] #{config} ..."
66 |             Command.run(command, :config => config, :daemonize => true)
67 |           end
68 |         end
69 |       
70 |         def sh(cmd)
71 |           log_info cmd
72 |           system(cmd)
73 |         end
74 |     end
75 |   end
76 | end
77 | 


--------------------------------------------------------------------------------
/lib/thin/controllers/service.sh.erb:
--------------------------------------------------------------------------------
 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 | :
40 | 


--------------------------------------------------------------------------------
/lib/thin/daemonizing.rb:
--------------------------------------------------------------------------------
  1 | require 'etc'
  2 | require 'daemons' unless Thin.win?
  3 | 
  4 | module Process
  5 |   # Returns +true+ the process identied by +pid+ is running.
  6 |   def running?(pid)
  7 |     Process.getpgid(pid) != -1
  8 |   rescue Errno::EPERM
  9 |     true
 10 |   rescue Errno::ESRCH
 11 |     false
 12 |   end
 13 |   module_function :running?
 14 | end
 15 | 
 16 | module Thin
 17 |   # Raised when the pid file already exist starting as a daemon.
 18 |   class PidFileExist < RuntimeError; end
 19 |   class PidFileNotFound < RuntimeError; end
 20 |   
 21 |   # Module included in classes that can be turned into a daemon.
 22 |   # Handle stuff like:
 23 |   # * storing the PID in a file
 24 |   # * redirecting output to the log file
 25 |   # * changing process privileges
 26 |   # * killing the process gracefully
 27 |   module Daemonizable
 28 |     attr_accessor :pid_file, :log_file
 29 |     
 30 |     def self.included(base)
 31 |       base.extend ClassMethods
 32 |     end
 33 | 
 34 |     def pid
 35 |       File.exist?(pid_file) && !File.zero?(pid_file) ? open(pid_file).read.to_i : nil
 36 |     end
 37 | 
 38 |     def kill(timeout = 60)
 39 |       if File.exist?(@pid_file)
 40 |         self.class.kill(@pid_file, timeout)
 41 |       end
 42 |     end
 43 | 
 44 |     # Turns the current script into a daemon process that detaches from the console.
 45 |     def daemonize
 46 |       raise PlatformNotSupported, 'Daemonizing is not supported on Windows'     if Thin.win?
 47 |       raise ArgumentError,        'You must specify a pid_file to daemonize' unless @pid_file
 48 |       
 49 |       remove_stale_pid_file
 50 |       
 51 |       pwd = Dir.pwd # Current directory is changed during daemonization, so store it
 52 |       
 53 |       # HACK we need to create the directory before daemonization to prevent a bug under 1.9
 54 |       #      ignoring all signals when the directory is created after daemonization.
 55 |       FileUtils.mkdir_p File.dirname(@pid_file)
 56 |       FileUtils.mkdir_p File.dirname(@log_file)
 57 |       
 58 |       Daemonize.daemonize(File.expand_path(@log_file), name)
 59 |       
 60 |       Dir.chdir(pwd)
 61 |       
 62 |       write_pid_file
 63 | 
 64 |       at_exit do
 65 |         log_info "Exiting!"
 66 |         remove_pid_file
 67 |       end
 68 |     end
 69 | 
 70 |     # Change privileges of the process
 71 |     # to the specified user and group.
 72 |     def change_privilege(user, group=user)
 73 |       log_info "Changing process privilege to #{user}:#{group}"
 74 |       
 75 |       uid, gid = Process.euid, Process.egid
 76 |       target_uid = Etc.getpwnam(user).uid
 77 |       target_gid = Etc.getgrnam(group).gid
 78 | 
 79 |       if uid != target_uid || gid != target_gid
 80 |         # Change PID file ownership
 81 |         File.chown(target_uid, target_gid, @pid_file) if File.exist?(@pid_file)
 82 | 
 83 |         # Change process ownership
 84 |         Process.initgroups(user, target_gid)
 85 |         Process::GID.change_privilege(target_gid)
 86 |         Process::UID.change_privilege(target_uid)
 87 | 
 88 |         # Correct environment variables
 89 |         ENV.store('USER', user)
 90 |         ENV.store('HOME', File.expand_path("~#{user}"))
 91 |       end
 92 |     rescue Errno::EPERM => e
 93 |       log_info "Couldn't change user and group to #{user}:#{group}: #{e}"
 94 |     end
 95 |     
 96 |     # Register a proc to be called to restart the server.
 97 |     def on_restart(&block)
 98 |       @on_restart = block
 99 |     end
100 |     
101 |     # Restart the server.
102 |     def restart
103 |       if @on_restart
104 |         log_info 'Restarting ...'
105 |         stop
106 |         remove_pid_file
107 |         @on_restart.call
108 |         EM.next_tick { exit! }
109 |       end
110 |     end
111 |     
112 |     module ClassMethods
113 |       # Send a QUIT or INT (if timeout is +0+) signal the process which
114 |       # PID is stored in +pid_file+.
115 |       # If the process is still running after +timeout+, KILL signal is
116 |       # sent.
117 |       def kill(pid_file, timeout=60)
118 |         if timeout == 0
119 |           send_signal('INT', pid_file, timeout)
120 |         else
121 |           send_signal('QUIT', pid_file, timeout)
122 |         end
123 |       end
124 |       
125 |       # Restart the server by sending HUP signal.
126 |       def restart(pid_file)
127 |         send_signal('HUP', pid_file)
128 |       end
129 | 
130 |       def monotonic_time
131 |         Process.clock_gettime(Process::CLOCK_MONOTONIC)
132 |       end
133 | 
134 |       # Send a +signal+ to the process which PID is stored in +pid_file+.
135 |       def send_signal(signal, pid_file, timeout=60)
136 |         if pid = read_pid_file(pid_file)
137 |           Logging.log_info "Sending #{signal} signal to process #{pid} ... "
138 | 
139 |           Process.kill(signal, pid)
140 | 
141 |           # This loop seems kind of racy to me...
142 |           started_at = monotonic_time
143 |           while Process.running?(pid)
144 |             sleep 0.1
145 |             raise Timeout::Error if (monotonic_time - started_at) > timeout
146 |           end
147 |         else
148 |           raise PidFileNotFound, "Can't stop process, no PID found in #{pid_file}"
149 |         end
150 |       rescue Timeout::Error
151 |         Logging.log_info "Timeout!"
152 |         force_kill(pid, pid_file)
153 |       rescue Interrupt
154 |         force_kill(pid, pid_file)
155 |       rescue Errno::ESRCH # No such process
156 |         Logging.log_info "process not found!"
157 |         force_kill(pid, pid_file)
158 |       end
159 |       
160 |       def force_kill(pid, pid_file)
161 |         Logging.log_info "Sending KILL signal to process #{pid} ... "
162 |         Process.kill("KILL", pid)
163 |         File.delete(pid_file) if File.exist?(pid_file)
164 |       end
165 |       
166 |       def read_pid_file(file)
167 |         if File.file?(file) && pid = File.read(file)
168 |           pid.to_i
169 |         else
170 |           nil
171 |         end
172 |       end
173 |     end
174 |     
175 |     protected
176 |       def remove_pid_file
177 |         File.delete(@pid_file) if @pid_file && File.exist?(@pid_file)
178 |       end
179 |     
180 |       def write_pid_file
181 |         log_info "Writing PID to #{@pid_file}"
182 |         open(@pid_file,"w") { |f| f.write(Process.pid) }
183 |         File.chmod(0644, @pid_file)
184 |       end
185 |       
186 |       # If PID file is stale, remove it.
187 |       def remove_stale_pid_file
188 |         if File.exist?(@pid_file)
189 |           if pid && Process.running?(pid)
190 |             raise PidFileExist, "#{@pid_file} already exists, seems like it's already running (process ID: #{pid}). " +
191 |                                 "Stop the process or delete #{@pid_file}."
192 |           else
193 |             log_info "Deleting stale PID file #{@pid_file}"
194 |             remove_pid_file
195 |           end
196 |         end
197 |       end
198 |   end
199 | end
200 | 


--------------------------------------------------------------------------------
/lib/thin/env.rb:
--------------------------------------------------------------------------------
 1 | 
 2 | module Thin
 3 |   module Env
 4 |     def self.with_defaults(env)
 5 |       if ::Rack.release >= "3"
 6 |         rack_env_class = Rack3
 7 |       else
 8 |         rack_env_class = Rack2
 9 |       end
10 | 
11 |       rack_env_class.env.merge(env)
12 |     end
13 |   end
14 | 
15 |   private
16 | 
17 |   class Rack2
18 |     def self.env
19 |       {
20 |         ::Thin::Request::RACK_VERSION      => ::Thin::VERSION::RACK,
21 |         ::Thin::Request::RACK_MULTITHREAD  => false,
22 |         ::Thin::Request::RACK_MULTIPROCESS => false,
23 |         ::Thin::Request::RACK_RUN_ONCE     => false
24 |       }
25 |     end
26 |   end
27 | 
28 |   class Rack3
29 |     def self.env
30 |       {}
31 |     end
32 |   end
33 | end
34 | 


--------------------------------------------------------------------------------
/lib/thin/headers.rb:
--------------------------------------------------------------------------------
 1 | module Thin
 2 |   # Raised when an header is not valid
 3 |   # and the server can not process it.
 4 |   class InvalidHeader < StandardError; end
 5 | 
 6 |   # Store HTTP header name-value pairs direcly to a string
 7 |   # and allow duplicated entries on some names.
 8 |   class Headers
 9 |     HEADER_FORMAT      = "%s: %s\r\n".freeze
10 |     ALLOWED_DUPLICATES = %w(set-cookie set-cookie2 warning www-authenticate).freeze
11 |     CR_OR_LF           = /[\r\n]/.freeze
12 |     
13 |     def initialize
14 |       @sent = {}
15 |       @out = []
16 |     end
17 |     
18 |     # Add key: value pair to the headers.
19 |     # Ignore if already sent and no duplicates are allowed
20 |     # for this +key+.
21 |     def []=(key, value)
22 |       downcase_key = key.downcase
23 |       if !@sent.has_key?(downcase_key) || ALLOWED_DUPLICATES.include?(downcase_key)
24 |         @sent[downcase_key] = true
25 |         value = case value
26 |                 when Time
27 |                   value.httpdate
28 |                 when NilClass
29 |                   return
30 |                 when CR_OR_LF
31 |                   raise InvalidHeader, "Header contains CR or LF"
32 |                 else
33 |                   value.to_s
34 |                 end
35 |         @out << HEADER_FORMAT % [key, value]
36 |       end
37 |     end
38 |     
39 |     def has_key?(key)
40 |       @sent[key.downcase]
41 |     end
42 |     
43 |     def to_s
44 |       @out.join
45 |     end
46 |   end
47 | end
48 | 


--------------------------------------------------------------------------------
/lib/thin/logging.rb:
--------------------------------------------------------------------------------
  1 | require 'logger'
  2 | 
  3 | module Thin
  4 |   # To be included in classes to allow some basic logging
  5 |   # that can be silenced (Logging.silent=) or made
  6 |   # more verbose.
  7 |   # Logging.trace=:  log all raw request and response and
  8 |   #                           messages logged with +trace+.
  9 |   # Logging.silent=: silence all log all log messages
 10 |   #                           altogether.
 11 |   module Logging
 12 |     # Simple formatter which only displays the message.
 13 |     # Taken from ActiveSupport
 14 |     class SimpleFormatter < Logger::Formatter
 15 |       def call(severity, timestamp, progname, msg)
 16 |         "#{timestamp} #{String === msg ? msg : msg.inspect}\n"
 17 |       end
 18 |     end
 19 | 
 20 |     @trace_logger = nil
 21 | 
 22 |     class << self
 23 |       attr_reader :logger
 24 |       attr_reader :trace_logger
 25 | 
 26 |       def trace=(enabled)
 27 |         if enabled
 28 |           @trace_logger ||= Logger.new(STDOUT)
 29 |         else
 30 |           @trace_logger = nil
 31 |         end
 32 |       end
 33 | 
 34 |       def trace?
 35 |         !@trace_logger.nil?
 36 |       end
 37 | 
 38 |       def silent=(shh)
 39 |         if shh
 40 |           @logger = nil
 41 |         else
 42 |           @logger ||= Logger.new(STDOUT)
 43 |         end
 44 |       end
 45 | 
 46 |       def silent?
 47 |         !@logger.nil?
 48 |       end
 49 | 
 50 |       def level
 51 |         @logger ? @logger.level : nil # or 'silent'
 52 |       end
 53 | 
 54 |       def level=(value)
 55 |         # If logging has been silenced, then re-enable logging
 56 |         @logger = Logger.new(STDOUT) if @logger.nil?
 57 |         @logger.level = value
 58 |       end
 59 | 
 60 |       # Allow user to specify a custom logger to use.
 61 |       # This object must respond to:
 62 |       # +level+, +level=+ and +debug+, +info+, +warn+, +error+, +fatal+
 63 |       def logger=(custom_logger)
 64 |         [ :level   ,
 65 |           :level=  ,
 66 |           :debug   ,
 67 |           :info    ,
 68 |           :warn    ,
 69 |           :error   ,
 70 |           :fatal   ,
 71 |           :unknown ,
 72 |         ].each do |method|
 73 |           if not custom_logger.respond_to?(method)
 74 |             raise ArgumentError, "logger must respond to #{method}"
 75 |           end
 76 |         end
 77 | 
 78 |         @logger = custom_logger
 79 |       end
 80 | 
 81 |       def trace_logger=(custom_tracer)
 82 |         [ :level   ,
 83 |           :level=  ,
 84 |           :debug   ,
 85 |           :info    ,
 86 |           :warn    ,
 87 |           :error   ,
 88 |           :fatal   ,
 89 |           :unknown ,
 90 |         ].each do |method|
 91 |           if not custom_tracer.respond_to?(method)
 92 |             raise ArgumentError, "trace logger must respond to #{method}"
 93 |           end
 94 |         end
 95 | 
 96 |         @trace_logger = custom_tracer
 97 |       end
 98 | 
 99 |       def log_msg(msg, level=Logger::INFO)
100 |         return unless @logger
101 |         @logger.add(level, msg)
102 |       end
103 | 
104 |       def trace_msg(msg)
105 |         return unless @trace_logger
106 |         @trace_logger.info(msg)
107 |       end
108 | 
109 |       # Provided for backwards compatibility.
110 |       # Callers should be using the +level+ (on the +Logging+ module
111 |       # or on the instance) to figure out what the log level is.
112 |       def debug?
113 |         self.level == Logger::DEBUG
114 |       end
115 |       def debug=(val)
116 |         self.level = (val ? Logger::DEBUG : Logger::INFO)
117 |       end
118 | 
119 |     end # module methods
120 | 
121 |     # Default logger to stdout.
122 |     self.logger           = Logger.new(STDOUT)
123 |     self.logger.level     = Logger::INFO
124 |     self.logger.formatter = Logging::SimpleFormatter.new
125 | 
126 |     def silent
127 |       Logging.silent?
128 |     end
129 | 
130 |     def silent=(value)
131 |       Logging.silent = value
132 |     end
133 | 
134 |     # Log a message if tracing is activated
135 |     def trace(msg=nil)
136 |       Logging.trace_msg(msg) if msg
137 |     end
138 |     module_function :trace
139 |     public :trace
140 | 
141 |     # Log a message at DEBUG level
142 |     def log_debug(msg=nil)
143 |       Logging.log_msg(msg || yield, Logger::DEBUG)
144 |     end
145 |     module_function :log_debug
146 |     public :log_debug
147 | 
148 |     # Log a message at INFO level
149 |     def log_info(msg)
150 |       Logging.log_msg(msg || yield, Logger::INFO)
151 |     end
152 |     module_function :log_info
153 |     public :log_info
154 | 
155 |     # Log a message at ERROR level (and maybe a backtrace)
156 |     def log_error(msg, e=nil)
157 |       log_msg = msg
158 |       if e
159 |         log_msg += ": #{e}\n\t" + e.backtrace.join("\n\t") + "\n"
160 |       end
161 |       Logging.log_msg(log_msg, Logger::ERROR)
162 |     end
163 |     module_function :log_error
164 |     public :log_error
165 | 
166 |     # For backwards compatibility
167 |     def log msg
168 |       STDERR.puts('#log has been deprecated, please use the ' \
169 |                   'log_level function instead (e.g. - log_info).')
170 |       log_info(msg)
171 |     end
172 | 
173 |   end
174 | end
175 | 


--------------------------------------------------------------------------------
/lib/thin/rackup/handler.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | module Thin
 4 |   module Rackup
 5 |     class Handler
 6 |       def self.run(app, **options)
 7 |         environment  = ENV['RACK_ENV'] || 'development'
 8 |         default_host = environment == 'development' ? 'localhost' : '0.0.0.0'
 9 | 
10 |         host = options.delete(:Host) || default_host
11 |         port = options.delete(:Port) || 8080
12 |         args = [host, port, app, options]
13 | 
14 |         server = ::Thin::Server.new(*args)
15 |         yield server if block_given?
16 | 
17 |         server.start
18 |       end
19 | 
20 |       def self.valid_options
21 |         environment  = ENV['RACK_ENV'] || 'development'
22 |         default_host = environment == 'development' ? 'localhost' : '0.0.0.0'
23 | 
24 |         {
25 |           "Host=HOST" => "Hostname to listen on (default: #{default_host})",
26 |           "Port=PORT" => "Port to listen on (default: 8080)",
27 |         }
28 |       end
29 |     end
30 |   end
31 | end
32 | 


--------------------------------------------------------------------------------
/lib/thin/request.rb:
--------------------------------------------------------------------------------
  1 | require 'tempfile'
  2 | require_relative './env'
  3 | 
  4 | module Thin
  5 |   # Raised when an incoming request is not valid
  6 |   # and the server can not process it.
  7 |   class InvalidRequest < IOError; end
  8 | 
  9 |   # A request sent by the client to the server.
 10 |   class Request
 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 |     MAX_HEADER        = 1024 * (80 + 32)
 16 | 
 17 |     INITIAL_BODY      = String.new
 18 |     # Force external_encoding of request's body to ASCII_8BIT
 19 |     INITIAL_BODY.encode!(Encoding::ASCII_8BIT) if INITIAL_BODY.respond_to?(:encode!) && defined?(Encoding::ASCII_8BIT)
 20 | 
 21 |     # Freeze some HTTP header names & values
 22 |     SERVER_SOFTWARE   = 'SERVER_SOFTWARE'.freeze
 23 |     SERVER_NAME       = 'SERVER_NAME'.freeze
 24 |     REQUEST_METHOD    = 'REQUEST_METHOD'.freeze
 25 |     LOCALHOST         = 'localhost'.freeze
 26 |     REQUEST_HTTP_VERSION      = 'thin.request_http_version'.freeze
 27 |     HTTP_1_0          = 'HTTP/1.0'.freeze
 28 |     REMOTE_ADDR       = 'REMOTE_ADDR'.freeze
 29 |     CONTENT_LENGTH    = 'CONTENT_LENGTH'.freeze
 30 |     CONNECTION        = 'HTTP_CONNECTION'.freeze
 31 |     KEEP_ALIVE_REGEXP = /\bkeep-alive\b/i.freeze
 32 |     CLOSE_REGEXP      = /\bclose\b/i.freeze
 33 |     HEAD              = 'HEAD'.freeze
 34 | 
 35 |     # Freeze some Rack header names
 36 |     RACK_INPUT        = 'rack.input'.freeze
 37 |     RACK_VERSION      = 'rack.version'.freeze
 38 |     RACK_ERRORS       = 'rack.errors'.freeze
 39 |     RACK_MULTITHREAD  = 'rack.multithread'.freeze
 40 |     RACK_MULTIPROCESS = 'rack.multiprocess'.freeze
 41 |     RACK_RUN_ONCE     = 'rack.run_once'.freeze
 42 |     ASYNC_CALLBACK    = 'async.callback'.freeze
 43 |     ASYNC_CLOSE       = 'async.close'.freeze
 44 | 
 45 |     # CGI-like request environment variables
 46 |     attr_reader :env
 47 | 
 48 |     # Unparsed data of the request
 49 |     attr_reader :data
 50 | 
 51 |     # Request body
 52 |     attr_reader :body
 53 | 
 54 |     def initialize
 55 |       @parser   = Thin::HttpParser.new
 56 |       @data     = String.new
 57 |       @nparsed  = 0
 58 |       @body     = StringIO.new(INITIAL_BODY.dup)
 59 |       @env      = Env.with_defaults({
 60 |         SERVER_SOFTWARE   => SERVER,
 61 |         SERVER_NAME       => LOCALHOST,
 62 | 
 63 |         # Rack stuff
 64 |         RACK_INPUT        => @body,
 65 |         RACK_ERRORS       => STDERR,
 66 |       })
 67 |     end
 68 | 
 69 |     # Parse a chunk of data into the request environment
 70 |     # Raises an +InvalidRequest+ if invalid.
 71 |     # Returns +true+ if the parsing is complete.
 72 |     def parse(data)
 73 |       if data.size > 0 && finished? # headers and body already fully satisfied. more data is erroneous.
 74 |         raise InvalidRequest, 'Content longer than specified'
 75 |       elsif @parser.finished?  # Header finished, can only be some more body
 76 |         @body << data
 77 |       else                  # Parse more header using the super parser
 78 |         @data << data
 79 |         raise InvalidRequest, 'Header longer than allowed' if @data.size > MAX_HEADER
 80 | 
 81 |         @nparsed = @parser.execute(@env, @data, @nparsed)
 82 | 
 83 |         # Transfer to a tempfile if body is very big
 84 |         move_body_to_tempfile if @parser.finished? && content_length > MAX_BODY
 85 |       end
 86 | 
 87 | 
 88 |       if finished?   # Check if header and body are complete
 89 |         @data = nil
 90 |         @body.rewind
 91 |         true         # Request is fully parsed
 92 |       else
 93 |         false        # Not finished, need more data
 94 |       end
 95 |     end
 96 | 
 97 |     # +true+ if headers and body are finished parsing
 98 |     def finished?
 99 |       @parser.finished? && @body.size >= content_length
100 |     end
101 | 
102 |     # Expected size of the body
103 |     def content_length
104 |       @env[CONTENT_LENGTH].to_i
105 |     end
106 | 
107 |     # Returns +true+ if the client expects the connection to be persistent.
108 |     def persistent?
109 |       # Clients and servers SHOULD NOT assume that a persistent connection
110 |       # is maintained for HTTP versions less than 1.1 unless it is explicitly
111 |       # signaled. (http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html)
112 |       if @env[REQUEST_HTTP_VERSION] == HTTP_1_0
113 |         @env[CONNECTION] =~ KEEP_ALIVE_REGEXP
114 | 
115 |       # HTTP/1.1 client intends to maintain a persistent connection unless
116 |       # a Connection header including the connection-token "close" was sent
117 |       # in the request
118 |       else
119 |         @env[CONNECTION].nil? || @env[CONNECTION] !~ CLOSE_REGEXP
120 |       end
121 |     end
122 | 
123 |     def remote_address=(address)
124 |       @env[REMOTE_ADDR] = address
125 |     end
126 | 
127 |     def threaded=(value)
128 |       @env[RACK_MULTITHREAD] = value
129 |     end
130 | 
131 |     def async_callback=(callback)
132 |       @env[ASYNC_CALLBACK] = callback
133 |       @env[ASYNC_CLOSE] = EventMachine::DefaultDeferrable.new
134 |     end
135 | 
136 |     def async_close
137 |       @async_close ||= @env[ASYNC_CLOSE]
138 |     end
139 | 
140 |     def head?
141 |       @env[REQUEST_METHOD] == HEAD
142 |     end
143 | 
144 |     # Close any resource used by the request
145 |     def close
146 |       @body.close! if @body.class == Tempfile
147 |     end
148 | 
149 |     private
150 |       def move_body_to_tempfile
151 |         current_body = @body
152 |         current_body.rewind
153 |         @body = Tempfile.new(BODY_TMPFILE)
154 |         @body.binmode
155 |         @body << current_body.read
156 |         @env[RACK_INPUT] = @body
157 |       end
158 |   end
159 | end
160 | 


--------------------------------------------------------------------------------
/lib/thin/response.rb:
--------------------------------------------------------------------------------
  1 | module Thin
  2 |   # A response sent to the client.
  3 |   class Response
  4 |     class Stream
  5 |       def initialize(writer)
  6 |         @read_closed = true
  7 |         @write_closed = false
  8 |         @writer = writer
  9 |       end
 10 | 
 11 |       def read(length = nil, outbuf = nil)
 12 |         raise ::IOError, 'not opened for reading' if @read_closed
 13 |       end
 14 | 
 15 |       def write(chunk)
 16 |         raise ::IOError, 'not opened for writing' if @write_closed
 17 | 
 18 |         @writer.call(chunk)
 19 |       end
 20 | 
 21 |       alias :<< :write
 22 | 
 23 |       def close
 24 |         @read_closed = @write_closed = true
 25 | 
 26 |         nil
 27 |       end
 28 | 
 29 |       def closed?
 30 |         @read_closed && @write_closed
 31 |       end
 32 | 
 33 |       def close_read
 34 |         @read_closed = true
 35 | 
 36 |         nil
 37 |       end
 38 | 
 39 |       def close_write
 40 |         @write_closed = true
 41 | 
 42 |         nil
 43 |       end
 44 | 
 45 |       def flush
 46 |         self
 47 |       end
 48 |     end
 49 | 
 50 |     CONNECTION     = 'connection'.freeze
 51 |     CLOSE          = 'close'.freeze
 52 |     KEEP_ALIVE     = 'keep-alive'.freeze
 53 |     SERVER         = 'server'.freeze
 54 |     CONTENT_LENGTH = 'content-length'.freeze
 55 | 
 56 |     PERSISTENT_STATUSES  = [100, 101].freeze
 57 | 
 58 |     #Error Responses
 59 |     ERROR            = [500, {'content-type' => 'text/plain'}, ['Internal server error']].freeze
 60 |     PERSISTENT_ERROR = [500, {'content-type' => 'text/plain', 'connection' => 'keep-alive', 'content-length' => "21"}, ['Internal server error']].freeze
 61 |     BAD_REQUEST      = [400, {'content-type' => 'text/plain'}, ['Bad Request']].freeze
 62 | 
 63 |     # Status code
 64 |     attr_accessor :status
 65 | 
 66 |     # Response body, must respond to +each+.
 67 |     attr_accessor :body
 68 | 
 69 |     # Headers key-value hash
 70 |     attr_reader   :headers
 71 | 
 72 |     def initialize
 73 |       @headers    = Headers.new
 74 |       @status     = 200
 75 |       @persistent = false
 76 |       @skip_body  = false
 77 |     end
 78 | 
 79 |     # String representation of the headers
 80 |     # to be sent in the response.
 81 |     def headers_output
 82 |       # Set default headers
 83 |       @headers[CONNECTION] = persistent? ? KEEP_ALIVE : CLOSE unless @headers.has_key?(CONNECTION)
 84 |       @headers[SERVER]     = Thin::NAME unless @headers.has_key?(SERVER)
 85 | 
 86 |       @headers.to_s
 87 |     end
 88 | 
 89 |     # Top header of the response,
 90 |     # containing the status code and response headers.
 91 |     def head
 92 |       "HTTP/1.1 #{@status} #{HTTP_STATUS_CODES[@status.to_i]}\r\n#{headers_output}\r\n"
 93 |     end
 94 | 
 95 |     if Thin.ruby_18?
 96 | 
 97 |       # Ruby 1.8 implementation.
 98 |       # Respects Rack specs.
 99 |       #
100 |       # See http://rack.rubyforge.org/doc/files/SPEC.html
101 |       def headers=(key_value_pairs)
102 |         key_value_pairs.each do |k, vs|
103 |           vs.each { |v| @headers[k] = v.chomp } if vs
104 |         end if key_value_pairs
105 |       end
106 | 
107 |     else
108 | 
109 |       # Ruby 1.9 doesn't have a String#each anymore.
110 |       # Rack spec doesn't take care of that yet, for now we just use
111 |       # +each+ but fallback to +each_line+ on strings.
112 |       # I wish we could remove that condition.
113 |       # To be reviewed when a new Rack spec comes out.
114 |       def headers=(key_value_pairs)
115 |         key_value_pairs.each do |k, vs|
116 |           next unless vs
117 |           if vs.is_a?(String)
118 |             vs.each_line { |v| @headers[k] = v.chomp }
119 |           else
120 |             vs.each { |v| @headers[k] = v.chomp }
121 |           end
122 |         end if key_value_pairs
123 |       end
124 | 
125 |     end
126 | 
127 |     # Close any resource used by the response
128 |     def close
129 |       @body.close if @body.respond_to?(:close)
130 |     end
131 | 
132 |     # Yields each chunk of the response.
133 |     # To control the size of each chunk
134 |     # define your own +each+ method on +body+.
135 |     def each(&block)
136 |       yield head
137 | 
138 |       unless @skip_body
139 |         if @body.is_a?(String)
140 |           yield @body
141 |         elsif @body.respond_to?(:each)
142 |           @body.each { |chunk| yield chunk }
143 |         else
144 |           @body.call(Stream.new(block))
145 |         end
146 |       end
147 |     end
148 | 
149 |     # Tell the client the connection should stay open
150 |     def persistent!
151 |       @persistent = true
152 |     end
153 | 
154 |     # Persistent connection must be requested as keep-alive
155 |     # from the server and have a content-length, or the response
156 |     # status must require that the connection remain open.
157 |     def persistent?
158 |       (@persistent && @headers.has_key?(CONTENT_LENGTH)) || PERSISTENT_STATUSES.include?(@status)
159 |     end
160 | 
161 |     def skip_body!
162 |       @skip_body = true
163 |     end
164 |   end
165 | end
166 | 


--------------------------------------------------------------------------------
/lib/thin/stats.rb:
--------------------------------------------------------------------------------
 1 | require 'erb'
 2 | 
 3 | module Thin
 4 |   module Stats
 5 |     # Rack adapter to log stats about a Rack application.
 6 |     class Adapter
 7 |       include ERB::Util
 8 |       
 9 |       def initialize(app, path='/stats')
10 |         @app  = app
11 |         @path = path
12 | 
13 |         @template = ERB.new(File.read(File.dirname(__FILE__) + '/stats.html.erb'))
14 |         
15 |         @requests          = 0
16 |         @requests_finished = 0
17 |         @start_time        = Time.now
18 |       end
19 |       
20 |       def call(env)
21 |         if env['PATH_INFO'].index(@path) == 0
22 |           serve(env)
23 |         else
24 |           log(env) { @app.call(env) }
25 |         end
26 |       end
27 |       
28 |       def log(env)
29 |         @requests += 1
30 |         @last_request = Rack::Request.new(env)
31 |         request_started_at = Time.now
32 |         
33 |         response = yield
34 |         
35 |         @requests_finished += 1
36 |         @last_request_time = Time.now - request_started_at
37 |         
38 |         response
39 |       end
40 |       
41 |       def serve(env)
42 |         body = @template.result(binding)
43 |         
44 |         [
45 |           200,
46 |           { 'content-type' => 'text/html' },
47 |           [body]
48 |         ]
49 |       end
50 |     end
51 |   end
52 | end
53 | 


--------------------------------------------------------------------------------
/lib/thin/statuses.rb:
--------------------------------------------------------------------------------
 1 | module Thin
 2 |   # Every standard HTTP code mapped to the appropriate message.
 3 |   # Stolent from Mongrel.
 4 |   HTTP_STATUS_CODES = {  
 5 |     100  => 'Continue', 
 6 |     101  => 'Switching Protocols', 
 7 |     200  => 'OK', 
 8 |     201  => 'Created', 
 9 |     202  => 'Accepted', 
10 |     203  => 'Non-Authoritative Information', 
11 |     204  => 'No Content', 
12 |     205  => 'Reset Content', 
13 |     206  => 'Partial Content', 
14 |     300  => 'Multiple Choices', 
15 |     301  => 'Moved Permanently', 
16 |     302  => 'Moved Temporarily', 
17 |     303  => 'See Other', 
18 |     304  => 'Not Modified', 
19 |     305  => 'Use Proxy', 
20 |     400  => 'Bad Request', 
21 |     401  => 'Unauthorized', 
22 |     402  => 'Payment Required', 
23 |     403  => 'Forbidden', 
24 |     404  => 'Not Found', 
25 |     405  => 'Method Not Allowed', 
26 |     406  => 'Not Acceptable', 
27 |     407  => 'Proxy Authentication Required', 
28 |     408  => 'Request Time-out', 
29 |     409  => 'Conflict', 
30 |     410  => 'Gone', 
31 |     411  => 'Length Required', 
32 |     412  => 'Precondition Failed', 
33 |     413  => 'Request Entity Too Large', 
34 |     414  => 'Request-URI Too Large', 
35 |     415  => 'Unsupported Media Type',
36 |     422  => 'Unprocessable Entity',
37 |     428  => 'Precondition Required',
38 |     429  => 'Too Many Requests',
39 |     431  => 'Request Header Fields Too Large',
40 |     500  => 'Internal Server Error',
41 |     501  => 'Not Implemented', 
42 |     502  => 'Bad Gateway', 
43 |     503  => 'Service Unavailable', 
44 |     504  => 'Gateway Time-out', 
45 |     505  => 'HTTP Version not supported',
46 |     511  => 'Network Authentication Required'
47 |   }
48 | end
49 | 


--------------------------------------------------------------------------------
/lib/thin/version.rb:
--------------------------------------------------------------------------------
 1 | module Thin
 2 |   # Raised when a feature is not supported on the
 3 |   # current platform.
 4 |   class PlatformNotSupported < RuntimeError; end
 5 |   
 6 |   module VERSION #:nodoc:
 7 |     MAJOR    = 1
 8 |     MINOR    = 8
 9 |     TINY     = 2
10 |     
11 |     STRING   = [MAJOR, MINOR, TINY].join('.')
12 |     
13 |     CODENAME = "Ruby Razor".freeze
14 |     
15 |     RACK     = [1, 0].freeze # Rack protocol version
16 |   end
17 |   
18 |   NAME    = 'thin'.freeze
19 |   SERVER  = "#{NAME} #{VERSION::STRING} codename #{VERSION::CODENAME}".freeze
20 |   
21 |   def self.win?
22 |     RUBY_PLATFORM =~ /mswin|mingw/
23 |   end
24 |   
25 |   def self.linux?
26 |     RUBY_PLATFORM =~ /linux/
27 |   end
28 |   
29 |   def self.ruby_18?
30 |     RUBY_VERSION =~ /^1\.8/
31 |   end
32 | end
33 | 


--------------------------------------------------------------------------------
/script/bleak:
--------------------------------------------------------------------------------
 1 | #!/usr/bin/env ruby-bleak-house
 2 | # Script to launch thin with Bleak House
 3 | # http://blog.evanweaver.com/files/doc/fauna/bleak_house/files/README.html
 4 | # 
 5 | # Will dump data to log/memlog
 6 | # Analyze the dump w/:
 7 | # 
 8 | #  bleak log/memlog
 9 | # 
10 | 
11 | module Kernel
12 |   def alias_method_chain(target, feature)
13 |     # Strip out punctuation on predicates or bang methods since
14 |     # e.g. target?_without_feature is not a valid method name.
15 |     aliased_target, punctuation = target.to_s.sub(/([?!=])$/, ''), $1
16 |     yield(aliased_target, punctuation) if block_given?
17 |     
18 |     with_method, without_method = "#{aliased_target}_with_#{feature}#{punctuation}", "#{aliased_target}_without_#{feature}#{punctuation}"
19 |     
20 |     alias_method without_method, target
21 |     alias_method target, with_method
22 |     
23 |     case
24 |       when public_method_defined?(without_method)
25 |         public target
26 |       when protected_method_defined?(without_method)
27 |         protected target
28 |       when private_method_defined?(without_method)
29 |         private target
30 |     end
31 |   end
32 | end
33 | 
34 | module BleakInstruments
35 |   module Connection
36 |     def self.included(base)
37 |       base.class_eval do
38 |         alias_method_chain :receive_data, :instrument
39 |         alias_method_chain :process, :instrument
40 |       end
41 |     end
42 |     
43 |     def receive_data_with_instrument(data)
44 |       receive_data_without_instrument(data)
45 |       $memlogger.snapshot($logfile, "connection/receive_data", false, 0.1)
46 |     end
47 |     
48 |     def process_with_instrument
49 |       process_without_instrument
50 |       $memlogger.snapshot($logfile, "connection/process", false, 0.1)
51 |     end
52 |   end
53 |   
54 |   module Backend
55 |     def self.included(base)
56 |       base.class_eval do
57 |         alias_method_chain :connect, :instrument
58 |         alias_method_chain :initialize_connection, :instrument
59 |       end
60 |     end
61 |     
62 |     def connect_with_instrument
63 |       connect_without_instrument
64 |       $memlogger.snapshot($logfile, "backend/connect", false, 0.1)
65 |     end
66 |     
67 |     def initialize_connection_with_instrument(connection)
68 |       initialize_connection_without_instrument(connection)
69 |       $memlogger.snapshot($logfile, "backend/initialize_connection", false, 0.1)
70 |     end
71 |   end
72 | end
73 | 
74 | require 'rubygems'
75 | require 'bleak_house'
76 | 
77 | $: << File.join(File.dirname(__FILE__), '..', 'lib')
78 | require 'thin'
79 | 
80 | Thin::Connection.send :include, BleakInstruments::Connection
81 | Thin::Backends::TcpServer.send :include, BleakInstruments::Backend
82 | 
83 | $memlogger = BleakHouse::Logger.new
84 | File.delete($logfile = File.expand_path("log/memlog")) rescue nil
85 | 
86 | load 'bin/thin'
87 | 


--------------------------------------------------------------------------------
/script/profile:
--------------------------------------------------------------------------------
 1 | #!/usr/bin/env ruby
 2 | # Script to profile thin using ruby-prof.
 3 | # Takes the same arguments as the thin script.
 4 | require 'rubygems'
 5 | require 'ruby-prof'
 6 | 
 7 | $: << File.join(File.dirname(__FILE__), '..', 'lib')
 8 | require 'thin'
 9 | 
10 | class Adapter
11 |   def call(env)
12 |     [200, {'content-type' => 'text/html', 'content-length' => '11'}, ['hello world']]
13 |   end
14 | end
15 | 
16 | # Profile the code
17 | result = RubyProf.profile do
18 |   Thin::Server.start('0.0.0.0', 3000) do
19 |     run Adapter.new
20 |   end
21 | end
22 | 
23 | # Print a graph profile to text
24 | printer = RubyProf::GraphPrinter.new(result)
25 | printer.print(STDOUT, 0)


--------------------------------------------------------------------------------
/script/valgrind:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | valgrind  --tool=memcheck --leak-check=yes --show-reachable=no --num-callers=15 --track-fds=yes ruby bin/thin $@


--------------------------------------------------------------------------------
/site/images/bullet.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/macournoyer/thin/de6b6188f1f1cd7645948b266e3861f82c9c58a1/site/images/bullet.gif


--------------------------------------------------------------------------------
/site/images/logo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/macournoyer/thin/de6b6188f1f1cd7645948b266e3861f82c9c58a1/site/images/logo.gif


--------------------------------------------------------------------------------
/site/images/logo.psd:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/macournoyer/thin/de6b6188f1f1cd7645948b266e3861f82c9c58a1/site/images/logo.psd


--------------------------------------------------------------------------------
/site/images/split.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/macournoyer/thin/de6b6188f1f1cd7645948b266e3861f82c9c58a1/site/images/split.gif


--------------------------------------------------------------------------------
/site/style.css:
--------------------------------------------------------------------------------
 1 | body { margin:0; padding:0; font: 12px Times, "Times New Roman"; text-align: center }
 2 | img { border:0 }
 3 | 
 4 | a { color: #666; text-decoration: none; border-bottom: solid 1px #ccc; }
 5 | a:hover { color: #333; border-bottom: solid 1px #aaa; }
 6 | 
 7 | acronym { border-bottom: dotted 1px #ccc; cursor: help; }
 8 | 
 9 | pre { font: 10px Courier, "Courier New"; background: #111; color: #fff; padding: 3px; border: solid 1px #ccc; }
10 | hr { background: url(/thin/images/split.gif) no-repeat; height: 30px; width: 100px; border: 0; margin: 40px auto; }
11 | 
12 | input, select, textarea { font: 9px Tahoma, Arial; }
13 | label { font-size: 11px; }
14 | 
15 | h1, h2, h3, h4 { margin: 12px 0 8px 0; padding: 0; }
16 | h3 { font-size: 14px; }
17 | h4 { font-size: 13px; }
18 | 
19 | ul#menu { padding: 4px; margin:0; background: #111; list-style: none; border-bottom: solid 1px #ccc; }
20 | ul#menu li { margin:0; display: inline; }
21 | ul#menu li a { color: #fff; text-decoration: none; padding: 6px; border: 0; }
22 | ul#menu li a:hover { text-decoration: underline; }
23 | 
24 | #container { width: 300px; margin: 0 auto; }
25 | 
26 | #header { padding: 30px 0 10px 0; }
27 | #header #logo { padding-bottom: 10px; }
28 | #header #tag_line { margin: 4px 0; font-size: 12px; letter-spacing: -1px; color: #333; }
29 | 
30 | #content { line-height: 16px; text-align: left; }
31 | #content h1 { margin: 40px 0 10px 0; padding: 0; text-align: center; }
32 | #content h2 { margin: 40px 0 10px 0; padding: 0; text-align: center; }
33 | #content h3 { margin: 40px 0 10px 0; padding: 0; text-align: center; }
34 | 
35 | #content ul { list-style-image: url(/thin/images/bullet.gif); margin: 0; padding: 0 20px; }
36 | #content li { padding: 1px 0; }
37 | 
38 | #footer { margin: 60px 0 20px 0; font-size: 10px; color: #666; }
39 | 
40 | #sidebar { position: absolute; right: 0; width: 400px; text-align: right; padding: 10px; }
41 | 
42 | ul.list { list-style: none; }
43 | ul.list li { margin: 0; padding: 1px 0; }
44 | 
45 | #content div.graph h3 { margin: 6px 0; }
46 | #content div.graph { text-align: center; }
47 | 
48 | #content em.filename { display: block; text-align: right; margin-bottom: -8px; }
49 | 
50 | .clear { clear: both; }
51 | 
52 | /*RDoc*/
53 | .dyn-source { display: none; }
54 | .path { text-align: center; font: 9px Tahoma, Arial; }
55 | .method-type { font: 9px Tahoma, Arial; color: #fff; background: #111; border: solid 1px #ccc; padding: 1px; }
56 | 


--------------------------------------------------------------------------------
/spec/backends/swiftiply_client_spec.rb:
--------------------------------------------------------------------------------
 1 | require 'spec_helper'
 2 | 
 3 | describe Backends::SwiftiplyClient do
 4 |   before do
 5 |     @backend = Backends::SwiftiplyClient.new('0.0.0.0', 3333)
 6 |     @backend.server = double('server').as_null_object
 7 |   end
 8 |   
 9 |   it "should connect" do
10 |     EventMachine.run do
11 |       @backend.connect
12 |       EventMachine.stop
13 |     end
14 |   end
15 |   
16 |   it "should disconnect" do
17 |     EventMachine.run do
18 |       @backend.connect
19 |       @backend.disconnect
20 |       EventMachine.stop
21 |     end
22 |   end
23 | end
24 | 
25 | describe SwiftiplyConnection do
26 |   before do
27 |     @connection = SwiftiplyConnection.new(nil)
28 |     @connection.backend = Backends::SwiftiplyClient.new('0.0.0.0', 3333)
29 |     @connection.backend.server = double('server').as_null_object
30 |   end
31 |   
32 |   it do
33 |     expect(@connection).to be_persistent
34 |   end
35 |   
36 |   it "should send handshake on connection_completed" do
37 |     expect(@connection).to receive(:send_data).with('swiftclient000000000d0500')
38 |     @connection.connection_completed
39 |   end
40 |   
41 |   it "should reconnect on unbind" do
42 |     allow(@connection.backend).to receive(:running?) { true }
43 |     allow(@connection).to receive(:rand) { 0 } # Make sure we don't wait
44 |     
45 |     expect(@connection).to receive(:reconnect).with('0.0.0.0', 3333)
46 |     
47 |     EventMachine.run do
48 |       @connection.unbind
49 |       EventMachine.add_timer(0) { EventMachine.stop }      
50 |     end
51 |   end
52 |   
53 |   it "should not reconnect when not running" do
54 |     allow(@connection.backend).to receive(:running?) { false }
55 |     expect(EventMachine).not_to receive(:add_timer)
56 |     @connection.unbind
57 |   end
58 |   
59 |   it "should have a host_ip" do
60 |     expect(@connection.send(:host_ip)).to eq([0, 0, 0, 0])
61 |   end
62 |   
63 |   it "should generate swiftiply_handshake based on key" do
64 |     expect(@connection.send(:swiftiply_handshake, 'key')).to eq('swiftclient000000000d0503key')
65 |   end
66 | end
67 | 


--------------------------------------------------------------------------------
/spec/backends/tcp_server_spec.rb:
--------------------------------------------------------------------------------
 1 | require 'spec_helper'
 2 | 
 3 | describe Backends::TcpServer do
 4 |   before do
 5 |     @backend = Backends::TcpServer.new('0.0.0.0', 3333)
 6 |   end
 7 |   
 8 |   it "should not use epoll" do
 9 |     @backend.no_epoll = true
10 |     expect(EventMachine).not_to receive(:epoll)
11 |     @backend.config
12 |   end
13 |   
14 |   it "should use epoll" do
15 |     expect(EventMachine).to receive(:epoll)
16 |     @backend.config
17 |   end
18 |   
19 |   it "should connect" do
20 |     EventMachine.run do
21 |       @backend.connect
22 |       EventMachine.stop
23 |     end
24 |   end
25 |   
26 |   it "should disconnect" do
27 |     EventMachine.run do
28 |       @backend.connect
29 |       @backend.disconnect
30 |       EventMachine.stop
31 |     end
32 |   end
33 | end
34 | 


--------------------------------------------------------------------------------
/spec/backends/unix_server_spec.rb:
--------------------------------------------------------------------------------
 1 | require 'spec_helper'
 2 | 
 3 | describe Backends::UnixServer do
 4 |   before do
 5 |     @backend = Backends::UnixServer.new('/tmp/thin-test.sock')
 6 |   end
 7 |   
 8 |   it "should connect" do
 9 |     EventMachine.run do
10 |       @backend.connect
11 |       EventMachine.stop
12 |     end
13 |   end
14 |   
15 |   it "should disconnect" do
16 |     EventMachine.run do
17 |       @backend.connect
18 |       @backend.disconnect
19 |       EventMachine.stop
20 |     end
21 |   end
22 |   
23 |   it "should remove socket file on close" do
24 |     @backend.close
25 |     expect(File.exist?('/tmp/thin-test.sock')).to be_falsey
26 |   end
27 | end
28 | 
29 | describe UnixConnection do
30 |   before do
31 |     @connection = UnixConnection.new(nil)
32 |   end
33 |   
34 |   it "should return 127.0.0.1 as remote_address" do
35 |     expect(@connection.remote_address).to eq('127.0.0.1')
36 |   end
37 | end
38 | 


--------------------------------------------------------------------------------
/spec/command_spec.rb:
--------------------------------------------------------------------------------
 1 | require 'spec_helper'
 2 | 
 3 | describe Command do
 4 |   before do
 5 |     Command.script = 'thin'
 6 |     @command = Command.new(:start, :port => 3000, :daemonize => true, :log => 'hi.log',
 7 |                            :require => %w(rubygems thin), :no_epoll => true)
 8 |   end
 9 |   
10 |   it 'should shellify command' do
11 |     out = @command.shellify
12 |     expect(out).to include('--port=3000', '--daemonize', '--log="hi.log"', 'thin start --')
13 |     expect(out).not_to include('--pid')
14 |   end
15 |   
16 |   it 'should shellify Array argument to multiple parameters' do
17 |     out = @command.shellify
18 |     expect(out).to include('--require="rubygems"', '--require="thin"')
19 |   end
20 | 
21 |   it 'should convert _ to - in option name' do
22 |     out = @command.shellify
23 |     expect(out).to include('--no-epoll')
24 |   end
25 | end


--------------------------------------------------------------------------------
/spec/configs/cluster.yml:
--------------------------------------------------------------------------------
 1 | ---
 2 | environment: production
 3 | timeout: 60
 4 | port:    5000
 5 | chdir:   spec/rails_app
 6 | log:     ../../log/thin.log      # relative to the chdir option
 7 | pid:     ../../tmp/pids/thin.pid # relative to the chdir option
 8 | servers: 3
 9 | address: 127.0.0.1
10 | 


--------------------------------------------------------------------------------
/spec/configs/single.yml:
--------------------------------------------------------------------------------
 1 | --- 
 2 | pid: tmp/pids/thin.pid
 3 | log: log/thin.log
 4 | timeout: 60
 5 | port: 6000
 6 | chdir: spec/rails_app
 7 | environment: production
 8 | daemonize: true
 9 | address: 127.0.0.1
10 | 


--------------------------------------------------------------------------------
/spec/configs/with_erb.yml:
--------------------------------------------------------------------------------
1 | ---
2 | timeout: <%= 30 %>
3 | port: <%= 4000 %>
4 | environment: <%= "production" %>
5 | 


--------------------------------------------------------------------------------
/spec/connection_spec.rb:
--------------------------------------------------------------------------------
  1 | require 'spec_helper'
  2 | 
  3 | describe Connection do
  4 |   before do
  5 |     allow(EventMachine).to receive(:send_data)
  6 |     @connection = Connection.new(double('EM').as_null_object)
  7 |     @connection.post_init
  8 |     @connection.backend = double("backend", :ssl? => false)
  9 |     @connection.app = proc do |env|
 10 |       [200, {}, ['body']]
 11 |     end
 12 |   end
 13 |   
 14 |   it "should parse on receive_data" do
 15 |     expect(@connection.request).to receive(:parse).with('GET')
 16 |     @connection.receive_data('GET')
 17 |   end
 18 | 
 19 |   it "should make a valid response on bad request" do
 20 |     allow(@connection.request).to receive(:parse).and_raise(InvalidRequest)
 21 |     expect(@connection).to receive(:post_process).with(Response::BAD_REQUEST)
 22 |     @connection.receive_data('')
 23 |   end
 24 | 
 25 |   it "should close connection on InvalidRequest error in receive_data" do
 26 |     allow(@connection.request).to receive(:parse).and_raise(InvalidRequest)
 27 |     allow(@connection.response).to receive(:persistent?) { false }
 28 |     @connection.can_persist!
 29 |     expect(@connection).to receive(:terminate_request)
 30 |     @connection.receive_data('')
 31 |   end
 32 | 
 33 |   it "should process when parsing complete" do
 34 |     expect(@connection.request).to receive(:parse).and_return(true)
 35 |     expect(@connection).to receive(:process)
 36 |     @connection.receive_data('GET')
 37 |   end
 38 | 
 39 |   it "should process at most once when request is larger than expected" do
 40 |     expect(@connection).to receive(:process).at_most(1)
 41 |     @connection.receive_data("POST / HTTP/1.1\r\nHost: localhost:3000\r\nContent-Length: 300\r\n\r\n")
 42 |     10.times { @connection.receive_data('X' * 1_000) }
 43 |   end
 44 | 
 45 |   it "should process" do
 46 |     @connection.process
 47 |   end
 48 | 
 49 |   it "should rescue error in process" do
 50 |     expect(@connection.app).to receive(:call).and_raise(StandardError)
 51 |     allow(@connection.response).to receive(:persistent?) { false }
 52 |     expect(@connection).to receive(:terminate_request)
 53 |     @connection.process
 54 |   end
 55 | 
 56 |   it "should make response on error" do
 57 |     expect(@connection.app).to receive(:call).and_raise(StandardError)
 58 |     expect(@connection).to receive(:post_process).with(Response::ERROR)
 59 |     @connection.process
 60 |   end
 61 | 
 62 |   it "should not close persistent connection on error" do
 63 |     expect(@connection.app).to receive(:call).and_raise(StandardError)
 64 |     allow(@connection.response).to receive(:persistent?) { true }
 65 |     @connection.can_persist!
 66 |     expect(@connection).to receive(:teminate_request).never
 67 |     @connection.process
 68 |   end
 69 | 
 70 |   it "should rescue Timeout error in process" do
 71 |     expect(@connection.app).to receive(:call).and_raise(Timeout::Error.new("timeout error not rescued"))
 72 |     @connection.process
 73 |   end
 74 |   
 75 |   it "should not return HTTP_X_FORWARDED_FOR as remote_address" do
 76 |     @connection.request.env['HTTP_X_FORWARDED_FOR'] = '1.2.3.4'
 77 |     allow(@connection).to receive(:socket_address) { "127.0.0.1" }
 78 |     expect(@connection.remote_address).to eq("127.0.0.1")
 79 |   end
 80 |   
 81 |   it "should return nil on error retrieving remote_address" do
 82 |     allow(@connection).to receive(:get_peername).and_raise(RuntimeError)
 83 |     expect(@connection.remote_address).to be_nil
 84 |   end
 85 |   
 86 |   it "should return nil on nil get_peername" do
 87 |     allow(@connection).to receive(:get_peername) { nil }
 88 |     expect(@connection.remote_address).to be_nil
 89 |   end
 90 |   
 91 |   it "should return nil on empty get_peername" do
 92 |     allow(@connection).to receive(:get_peername) { '' }
 93 |     expect(@connection.remote_address).to be_nil
 94 |   end
 95 |   
 96 |   it "should return remote_address" do
 97 |     allow(@connection).to receive(:get_peername) do
 98 |       Socket.pack_sockaddr_in(3000, '127.0.0.1')
 99 |     end
100 |     expect(@connection.remote_address).to eq('127.0.0.1')
101 |   end
102 |   
103 |   it "should not be persistent" do
104 |     expect(@connection).not_to be_persistent
105 |   end
106 | 
107 |   it "should be persistent when response is and allowed" do
108 |     allow(@connection.response).to receive(:persistent?) { true }
109 |     @connection.can_persist!
110 |     expect(@connection).to be_persistent
111 |   end
112 | 
113 |   it "should not be persistent when response is but not allowed" do
114 |     @connection.response.persistent!
115 |     expect(@connection).not_to be_persistent
116 |   end
117 |   
118 |   it "should return empty body on HEAD request" do
119 |     expect(@connection.request).to receive(:head?).and_return(true)
120 |     expect(@connection).to receive(:send_data).once # Only once for the headers
121 |     @connection.process
122 |   end
123 |   
124 |   it "should set request env as rack.multithread" do
125 |     expect(EventMachine).to receive(:defer)
126 |     
127 |     @connection.threaded = true
128 |     @connection.process
129 |     
130 |     expect(@connection.request.env["rack.multithread"]).to eq(true)
131 |   end
132 |   
133 |   it "should set as threaded when app.deferred? is true" do
134 |     expect(@connection.app).to receive(:deferred?).and_return(true)
135 |     expect(@connection).to be_threaded
136 |   end
137 |   
138 |   it "should not set as threaded when app.deferred? is false" do
139 |     expect(@connection.app).to receive(:deferred?).and_return(false)
140 |     expect(@connection).not_to be_threaded
141 |   end
142 | 
143 |   it "should not set as threaded when app do not respond to deferred?" do
144 |     expect(@connection).not_to be_threaded
145 |   end
146 | 
147 |   it "should have correct SERVER_PORT when using ssl" do
148 |     @connection.backend = double("backend", :ssl? => true, :port => 443)
149 | 
150 |     @connection.process
151 | 
152 |     expect(@connection.request.env["SERVER_PORT"]).to eq("443")
153 |   end
154 | end
155 | 


--------------------------------------------------------------------------------
/spec/controllers/controller_spec.rb:
--------------------------------------------------------------------------------
  1 | require 'spec_helper'
  2 | require 'ostruct'
  3 | include Controllers
  4 | 
  5 | describe Controller, 'start' do
  6 |   before do
  7 |     @controller = Controller.new(:address              => '0.0.0.0',
  8 |                                  :port                 => 3000,
  9 |                                  :pid                  => 'thin.pid',
 10 |                                  :log                  => 'thin.log',
 11 |                                  :timeout              => 60,
 12 |                                  :max_conns            => 2000,
 13 |                                  :max_persistent_conns => 1000,
 14 |                                  :adapter              => 'rails')
 15 |     
 16 |     @server = OpenStruct.new
 17 |     @adapter = OpenStruct.new
 18 |     
 19 |     expect(Server).to receive(:new).with('0.0.0.0', 3000, @controller.options).and_return(@server)
 20 |     expect(@server).to receive(:config)
 21 |     allow(Rack::Adapter::Rails).to receive(:new) { @adapter }
 22 |   end
 23 |   
 24 |   it "should configure server" do
 25 |     @controller.start
 26 |     
 27 |     expect(@server.app).to eq(@adapter)
 28 |     expect(@server.pid_file).to eq('thin.pid')
 29 |     expect(@server.log_file).to eq('thin.log')
 30 |     expect(@server.maximum_connections).to eq(2000)
 31 |     expect(@server.maximum_persistent_connections).to eq(1000)
 32 |   end
 33 |   
 34 |   it "should start as daemon" do
 35 |     @controller.options[:daemonize] = true
 36 |     @controller.options[:user] = true
 37 |     @controller.options[:group] = true
 38 |     
 39 |     expect(@server).to receive(:daemonize)
 40 |     expect(@server).to receive(:change_privilege)
 41 | 
 42 |     @controller.start
 43 |   end
 44 |   
 45 |   it "should configure Rails adapter" do
 46 |     expect(Rack::Adapter::Rails).to receive(:new).with(@controller.options.merge(:root => nil))
 47 |     
 48 |     @controller.start
 49 |   end
 50 |   
 51 |   it "should mount app under :prefix" do
 52 |     @controller.options[:prefix] = '/app'
 53 |     @controller.start
 54 |     
 55 |     expect(@server.app.class).to eq(Rack::URLMap)
 56 |   end
 57 | 
 58 |   it "should mount Stats adapter under :stats" do
 59 |     @controller.options[:stats] = '/stats'
 60 |     @controller.start
 61 |     
 62 |     expect(@server.app.class).to eq(Stats::Adapter)
 63 |   end
 64 |   
 65 |   it "should load app from Rack config" do
 66 |     @controller.options[:rackup] = File.dirname(__FILE__) + '/../../example/config.ru'
 67 |     @controller.start
 68 |     
 69 |     expect(@server.app.class).to eq(Proc)
 70 |   end
 71 | 
 72 |   it "should load app from ruby file" do
 73 |     @controller.options[:rackup] = File.dirname(__FILE__) + '/../../example/myapp.rb'
 74 |     @controller.start
 75 |     
 76 |     expect(@server.app).to eq(Myapp)
 77 |   end
 78 | 
 79 |   it "should throwup if rackup is not a .ru or .rb file" do
 80 |     expect do
 81 |       @controller.options[:rackup] = File.dirname(__FILE__) + '/../../example/myapp.foo'
 82 |       @controller.start
 83 |     end.to raise_error(RuntimeError, /please/)
 84 |   end
 85 |   
 86 |   it "should set server as threaded" do
 87 |     @controller.options[:threaded] = true
 88 |     @controller.start
 89 |     
 90 |     expect(@server.threaded).to be_truthy
 91 |   end
 92 |   
 93 |   it "should set RACK_ENV" do
 94 |     @controller.options[:rackup] = File.dirname(__FILE__) + '/../../example/config.ru'
 95 |     @controller.options[:environment] = "lolcat"
 96 |     @controller.start
 97 |     
 98 |     expect(ENV['RACK_ENV']).to eq("lolcat")
 99 |   end
100 |     
101 | end
102 | 
103 | describe Controller do
104 |   before do
105 |     @controller = Controller.new(:pid => 'thin.pid', :timeout => 10)
106 |     allow(@controller).to receive(:wait_for_file)
107 |   end
108 |   
109 |   it "should stop" do
110 |     expect(Server).to receive(:kill).with('thin.pid', 10)
111 |     @controller.stop
112 |   end
113 |   
114 |   it "should restart" do
115 |     expect(Server).to receive(:restart).with('thin.pid')
116 |     @controller.restart
117 |   end
118 |   
119 |   it "should write configuration file" do
120 |     silence_stream(STDOUT) do
121 |       Controller.new(:config => 'test.yml', :port => 5000, :address => '127.0.0.1').config
122 |     end
123 | 
124 |     expect(File.read('test.yml')).to include('port: 5000', 'address: 127.0.0.1')
125 |     expect(File.read('test.yml')).not_to include('config: ')
126 | 
127 |     File.delete('test.yml')
128 |   end
129 | end
130 | 


--------------------------------------------------------------------------------
/spec/controllers/service_spec.rb:
--------------------------------------------------------------------------------
 1 | require 'spec_helper'
 2 | include Controllers
 3 | 
 4 | describe Service do
 5 |   before(:all) do
 6 |     silence_warnings do
 7 |       Service::INITD_PATH          = 'tmp/sandbox' + Service::INITD_PATH
 8 |       Service::DEFAULT_CONFIG_PATH = 'tmp/sandbox' + Service::DEFAULT_CONFIG_PATH
 9 |     end
10 |   end
11 | 
12 |   before do
13 |     allow(Thin).to receive(:linux?) { true }
14 |     FileUtils.mkdir_p 'tmp/sandbox'
15 | 
16 |     @service = Service.new(:all => 'spec/configs')
17 |   end
18 | 
19 |   it "should call command for each config file" do
20 |     expect(Command).to receive(:run).with(:start, :config => 'spec/configs/cluster.yml', :daemonize => true)
21 |     expect(Command).to receive(:run).with(:start, :config => 'spec/configs/single.yml', :daemonize => true)
22 |     expect(Command).to receive(:run).with(:start, :config => 'spec/configs/with_erb.yml', :daemonize => true)
23 | 
24 |     @service.start
25 |   end
26 | 
27 |   it "should create /etc/init.d/thin file when calling install" do
28 |     @service.install
29 | 
30 |     expect(File.exist?(Service::INITD_PATH)).to be_truthy
31 |     script_name = File.directory?('/etc/rc.d') ?
32 |       '/etc/rc.d/thin' : '/etc/init.d/thin'
33 |     expect(File.read(Service::INITD_PATH)).to include('CONFIG_PATH=tmp/sandbox/etc/thin',
34 |                                                   'SCRIPT_NAME=tmp/sandbox' + script_name,
35 |                                                   'DAEMON=' + Command.script)
36 |   end
37 | 
38 |   it "should create /etc/thin dir when calling install" do
39 |     @service.install
40 | 
41 |     expect(File.directory?(Service::DEFAULT_CONFIG_PATH)).to be_truthy
42 |   end
43 | 
44 |   it "should include specified path in /etc/init.d/thin script" do
45 |     @service.install('tmp/sandbox/usr/thin')
46 | 
47 |     expect(File.read(Service::INITD_PATH)).to include('CONFIG_PATH=tmp/sandbox/usr/thin')
48 |   end
49 | 
50 |   after do
51 |     FileUtils.rm_rf 'tmp/sandbox'
52 |   end
53 | end
54 | 


--------------------------------------------------------------------------------
/spec/daemonizing_spec.rb:
--------------------------------------------------------------------------------
  1 | require 'spec_helper'
  2 | 
  3 | require 'timeout'
  4 | require "tmpdir"
  5 | 
  6 | class TestServer
  7 |   include Logging
  8 |   include Daemonizable
  9 | 
 10 |   def stop
 11 |   end
 12 | 
 13 |   def name
 14 |     'Thin test server'
 15 |   end
 16 | end
 17 | 
 18 | describe 'Daemonizing' do
 19 |   let(:log_file) {File.join(@root, "test_server.log")}
 20 |   let(:pid_file) {File.join(@root, "test.pid")}
 21 | 
 22 |   around do |example|
 23 |     Dir.mktmpdir do |root|
 24 |       @root = root
 25 | 
 26 |       # Ensure the log file exists with the correct permissions:
 27 |       File.open(log_file, "w+") {}
 28 | 
 29 |       example.run
 30 |       @root = nil
 31 |     end
 32 |   end
 33 | 
 34 |   subject(:server) do
 35 |     raise "No root directory" unless @root
 36 | 
 37 |     TestServer.new.tap do |server|
 38 |       server.log_file = log_file
 39 |       server.pid_file = pid_file
 40 |     end
 41 |   end
 42 | 
 43 |   it 'should have a pid file' do
 44 |     expect(subject).to respond_to(:pid_file)
 45 |     expect(subject).to respond_to(:pid_file=)
 46 |   end
 47 | 
 48 |   it 'should create a pid file' do
 49 |     fork do
 50 |       subject.daemonize
 51 |       sleep
 52 |     end
 53 | 
 54 |     wait_for_server_to_start
 55 | 
 56 |     subject.kill
 57 |   end
 58 |   
 59 |   it 'should redirect stdio to a log file' do
 60 |     pid = fork do
 61 |       subject.daemonize
 62 | 
 63 |       puts "simple puts"
 64 |       STDERR.puts "STDERR.puts"
 65 |       STDOUT.puts "STDOUT.puts"
 66 | 
 67 |       sleep
 68 |     end
 69 | 
 70 |     wait_for_server_to_start
 71 | 
 72 |     log = File.read(log_file)
 73 |     expect(log).to include('simple puts', 'STDERR.puts', 'STDOUT.puts')
 74 | 
 75 |     server.kill
 76 |   end
 77 |   
 78 |   it 'should change privilege' do
 79 |     pid = fork do
 80 |       subject.daemonize
 81 |       subject.change_privilege('root', 'admin')
 82 |     end
 83 | 
 84 |     _, status = Process.wait2(pid)
 85 | 
 86 |     expect(status).to be_a_success
 87 |   end
 88 |   
 89 |   it 'should kill process in pid file' do
 90 |     expect(File.exist?(subject.pid_file)).to be_falsey
 91 | 
 92 |     pid = fork do
 93 |       subject.daemonize
 94 |       sleep
 95 |     end
 96 | 
 97 |     wait_for_server_to_start
 98 | 
 99 |     expect(File.exist?(subject.pid_file)).to be_truthy
100 | 
101 |     silence_stream STDOUT do
102 |       subject.kill(1)
103 |     end
104 | 
105 |     Process.wait(pid)
106 |     expect(File.exist?(subject.pid_file)).to be_falsey
107 |   end
108 |   
109 |   it 'should force kill process in pid file' do
110 |     fork do
111 |       subject.daemonize
112 |       sleep
113 |     end
114 | 
115 |     wait_for_server_to_start
116 | 
117 |     subject.kill(0)
118 | 
119 |     expect(File.exist?(subject.pid_file)).to be_falsey
120 |   end
121 |   
122 |   it 'should send kill signal if timeout' do
123 |     fork do
124 |       subject.daemonize
125 |       sleep
126 |     end
127 | 
128 |     wait_for_server_to_start
129 | 
130 |     pid = subject.pid
131 | 
132 |     subject.kill(10)
133 | 
134 |     expect(File.exist?(subject.pid_file)).to be_falsey
135 |     expect(Process.running?(pid)).to be_falsey
136 |   end
137 |   
138 |   it "should restart" do
139 |     fork do
140 |       subject.on_restart {}
141 |       subject.daemonize
142 |       sleep 5
143 |     end
144 | 
145 |     wait_for_server_to_start
146 | 
147 |     silence_stream STDOUT do
148 |       TestServer.restart(subject.pid_file)
149 |     end
150 | 
151 |     expect { sleep 0.1 while File.exist?(subject.pid_file) }.to take_less_then(20)
152 |   end
153 |   
154 |   it "should ignore if no restart block specified" do
155 |     subject.restart
156 |   end
157 |   
158 |   it "should not restart when not running" do
159 |     silence_stream STDOUT do
160 |       subject.restart
161 |     end
162 |   end
163 |   
164 |   it "should exit and raise if pid file already exist" do
165 |     fork do
166 |       subject.daemonize
167 |       sleep 5
168 |     end
169 | 
170 |     wait_for_server_to_start
171 | 
172 |     expect { subject.daemonize }.to raise_error(PidFileExist)
173 | 
174 |     expect(File.exist?(subject.pid_file)).to be_truthy
175 |   end
176 | 
177 |   it "should raise if no pid file" do
178 |     expect do
179 |       TestServer.kill("donotexist", 0)
180 |     end.to raise_error(PidFileNotFound)
181 |   end
182 | 
183 |   it "should should delete pid file if stale" do
184 |     # Create a file w/ a PID that does not exist
185 |     File.open(subject.pid_file, 'w') { |f| f << 999999999 }
186 |     
187 |     subject.send(:remove_stale_pid_file)
188 |     
189 |     expect(File.exist?(subject.pid_file)).to be_falsey
190 |   end
191 | 
192 |   private
193 | 
194 |   def wait_for_server_to_start
195 |     count = 1
196 |     
197 |     while true
198 |       break if File.exist?(subject.pid_file)
199 | 
200 |       sleep(count * 0.1)
201 | 
202 |       if count > 10
203 |         $stderr.puts "Dumping log file #{subject.log_file}:"
204 |         File.foreach(subject.log_file) do |line|
205 |           $stderr.puts line
206 |         end
207 | 
208 |         raise "Server did not start"
209 |       end
210 | 
211 |       count += 1
212 |     end
213 |   end
214 | end
215 | 


--------------------------------------------------------------------------------
/spec/headers_spec.rb:
--------------------------------------------------------------------------------
 1 | require 'spec_helper'
 2 | 
 3 | describe Headers do
 4 |   before do
 5 |     @headers = Headers.new
 6 |   end
 7 |   
 8 |   it 'should allow duplicate on some fields' do
 9 |     @headers['Set-Cookie'] = 'twice'
10 |     @headers['Set-Cookie'] = 'is cooler the once'
11 |     
12 |     expect(@headers.to_s).to eq("Set-Cookie: twice\r\nSet-Cookie: is cooler the once\r\n")
13 |   end
14 |   
15 |   it 'should overwrite value on non duplicate fields' do
16 |     @headers['Host'] = 'this is unique'
17 |     @headers['Host'] = 'so is this'
18 | 
19 |     expect(@headers.to_s).to eq("Host: this is unique\r\n")
20 |   end
21 |   
22 |   it 'should output to string' do
23 |     @headers['Host'] = 'localhost:3000'
24 |     @headers['Set-Cookie'] = 'twice'
25 |     @headers['Set-Cookie'] = 'is cooler the once'
26 |     
27 |     expect(@headers.to_s).to eq("Host: localhost:3000\r\nSet-Cookie: twice\r\nSet-Cookie: is cooler the once\r\n")
28 |   end
29 | 
30 |   it 'should ignore nil values' do
31 |     @headers['Something'] = nil
32 |     expect(@headers.to_s).not_to include('Something: ')
33 |   end
34 | 
35 |   it 'should format Time values correctly' do
36 |     time = Time.now
37 |     @headers['Modified-At'] = time
38 |     expect(@headers.to_s).to include("Modified-At: #{time.httpdate}")
39 |   end
40 | 
41 |   it 'should format Integer values correctly' do
42 |     @headers['X-Number'] = 32
43 |     expect(@headers.to_s).to include("X-Number: 32")
44 |   end
45 | 
46 |   it 'should not allow CRLF' do
47 |     expect { @headers['Bad'] = "a\r\nSet-Cookie: injected=value" }.to raise_error(InvalidHeader)
48 |   end
49 | 
50 |   it 'should not allow CR' do
51 |     expect { @headers['Bad'] = "a\rSet-Cookie: injected=value" }.to raise_error(InvalidHeader)
52 |   end
53 | 
54 |   it 'should not allow LF' do
55 |     expect { @headers['Bad'] = "a\nSet-Cookie: injected=value" }.to raise_error(InvalidHeader)
56 |   end
57 | end


--------------------------------------------------------------------------------
/spec/logging_spec.rb:
--------------------------------------------------------------------------------
  1 | require 'spec_helper'
  2 | 
  3 | ##
  4 | # Dummy class, so that we can mix in the Logging module and test it.
  5 | #
  6 | class TestLogging
  7 |   include Logging
  8 | end
  9 | 
 10 | describe Logging do
 11 |   subject {TestLogging.new}
 12 | 
 13 |   after do
 14 |     Logging.silent = true
 15 |     Logging.debug = false
 16 |     Logging.trace = false
 17 |   end
 18 | 
 19 |   describe "when setting a custom logger" do
 20 | 
 21 |     it "should not accept a logger object that is not sane" do
 22 |       expect { Logging.logger = "" }.to raise_error(ArgumentError)
 23 |     end
 24 | 
 25 |     it "should accept a legit custom logger object" do
 26 |       expect { Logging.logger = Logger.new(STDOUT) }.to_not raise_error
 27 |     end
 28 | 
 29 |   end
 30 | 
 31 |   describe "logging routines (with a custom logger)" do
 32 | 
 33 |     before :each do
 34 |       @readpipe, @writepipe = IO.pipe
 35 |       @custom_logger = Logger.new(@writepipe)
 36 |       Logging.logger = @custom_logger
 37 |       Logging.level  = Logger::INFO
 38 |     end
 39 | 
 40 |     after :each do
 41 |       [@readpipe, @writepipe].each do |pipe|
 42 |         pipe.close if pipe
 43 |       end
 44 |     end
 45 | 
 46 |     #
 47 |     #
 48 |     it "at log level DEBUG should output logs at debug level" do
 49 |       Logging.debug = true
 50 |       subject.log_debug("hi")
 51 | 
 52 |       str = nil
 53 |       expect { str = @readpipe.read_nonblock(512) }.to_not raise_error
 54 |       expect(str).not_to be_nil
 55 |     end
 56 | 
 57 |     #
 58 |     #
 59 |     it "at log level NOT DEBUG should NOT output logs at debug level" do
 60 |       Logging.debug = false
 61 |       subject.log_debug("hiya")
 62 | 
 63 |       expect do
 64 |         @readpipe.read_nonblock(512)
 65 |       end.to raise_error(IO::EAGAINWaitReadable)
 66 |     end
 67 | 
 68 |     #
 69 |     #
 70 |     it "should be usable (at the module level) for logging" do
 71 |       expect(@custom_logger).to receive(:add)
 72 |       Logging.log_msg("hey")
 73 |     end
 74 | 
 75 |     # These should be the last test we run for the 'log' functionality
 76 |     #
 77 |     it "should not log messages if silenced via module method" do
 78 |       Logging.silent = true
 79 |       subject.log_info("hola")
 80 |       expect do
 81 |         @readpipe.read_nonblock(512)
 82 |       end.to raise_error(IO::EAGAINWaitReadable)
 83 |     end
 84 | 
 85 |     it "should not log anything if silenced via module methods" do
 86 |       Logging.silent = true
 87 |       Logging.log_msg("hi")
 88 |       expect do
 89 |         @readpipe.read_nonblock(512)
 90 |       end.to raise_error(IO::EAGAINWaitReadable)
 91 |     end
 92 | 
 93 |     it "should not log anything if silenced via instance methods" do
 94 |       subject.silent = true
 95 |       subject.log_info("hello")
 96 |       expect do
 97 |         @readpipe.read_nonblock(512)
 98 |       end.to raise_error(IO::EAGAINWaitReadable)
 99 |     end
100 | 
101 |   end # Logging tests (with custom logger)
102 | 
103 |   describe "logging routines (with NO custom logger)" do
104 | 
105 |     it "should log at debug level if debug logging is enabled " do
106 |       Logging.debug = true
107 |       out = with_redirected_stdout do
108 |         subject.log_debug("Hey")
109 |       end
110 | 
111 |       expect(out.include?("Hey")).to be_truthy
112 |       expect(out.include?("DEBUG")).to be_truthy
113 |     end
114 | 
115 |     it "should be usable (at the module level) for logging" do
116 |       out = with_redirected_stdout do
117 |         Logging.log_msg("Hey")
118 |       end
119 | 
120 |       expect(out.include?("Hey")).to be_truthy
121 |     end
122 | 
123 |   end
124 | 
125 |   describe "trace routines (with custom trace logger)" do
126 | 
127 |     before :each do
128 |       @custom_tracer = Logger.new(STDERR)
129 |       Logging.trace_logger = @custom_tracer
130 |     end
131 | 
132 |     it "should NOT emit trace messages if tracing is disabled" do
133 |       Logging.trace = false
134 |       expect(@custom_tracer).not_to receive(:info)
135 |       subject.trace("howdy")
136 |     end
137 | 
138 |     it "should emit trace messages when tracing is enabled" do
139 |       Logging.trace = true
140 |       expect(@custom_tracer).to receive(:info)
141 | 
142 |       subject.trace("aloha")
143 |     end
144 | 
145 |   end # Tracer tests (with custom tracer)
146 | 
147 |   describe "tracing routines (with NO custom logger)" do
148 | 
149 |     it "should emit trace messages if tracing is enabled " do
150 |       Logging.trace = true
151 |       out = with_redirected_stdout do
152 |         subject.trace("Hey")
153 |       end
154 | 
155 |       expect(out.include?("Hey")).to be_truthy
156 |     end
157 | 
158 |     it "should be usable (at the module level) for logging" do
159 |       Logging.trace = true
160 |       out = with_redirected_stdout do
161 |         Logging.trace_msg("hey")
162 |       end
163 | 
164 |       expect(out.include?("hey")).to be_truthy
165 |     end
166 | 
167 |   end # tracer tests (no custom logger)
168 | 
169 | end
170 | 


--------------------------------------------------------------------------------
/spec/perf/request_perf_spec.rb:
--------------------------------------------------------------------------------
 1 | require 'spec_helper'
 2 | 
 3 | describe Request, 'performance' do
 4 |   it "should be faster then #{max_parsing_time = 0.0002} RubySeconds" do
 5 |     body = <<-EOS.chomp.gsub("\n", "\r\n")
 6 | POST /postit HTTP/1.1
 7 | Host: localhost:3000
 8 | User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1.9) Gecko/20071025 Firefox/2.0.0.9
 9 | Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
10 | Accept-Language: en-us,en;q=0.5
11 | Accept-Encoding: gzip,deflate
12 | Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
13 | Keep-Alive: 300
14 | Connection: keep-alive
15 | Content-Type: text/html
16 | Content-Length: 37
17 | 
18 | hi=there&name=marc&email=macournoyer@gmail.com
19 | EOS
20 | 
21 |     expect { R(body) }.to be_faster_then(max_parsing_time)
22 |   end
23 | 
24 |   it 'should be comparable to Mongrel parser' do
25 |     require 'http11'
26 | 
27 |     body = <<-EOS.chomp.gsub("\n", "\r\n")
28 | POST /postit HTTP/1.1
29 | Host: localhost:3000
30 | User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1.9) Gecko/20071025 Firefox/2.0.0.9
31 | Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
32 | Accept-Language: en-us,en;q=0.5
33 | Accept-Encoding: gzip,deflate
34 | Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
35 | Keep-Alive: 300
36 | Connection: keep-alive
37 | Content-Type: text/html
38 | Content-Length: 37
39 | 
40 | hi=there&name=marc&email=macournoyer@gmail.com
41 | EOS
42 | 
43 |     tests = 10_000
44 |     puts
45 |     Benchmark.bmbm(10) do |results|
46 |       results.report("mongrel:") { tests.times { Mongrel::HttpParser.new.execute({}, body.dup, 0) } }
47 |       results.report("thin:") { tests.times { Thin::HttpParser.new.execute({'rack.input' => StringIO.new}, body.dup, 0) } }
48 |     end
49 |   end if ENV['BM']
50 | end


--------------------------------------------------------------------------------
/spec/perf/response_perf_spec.rb:
--------------------------------------------------------------------------------
 1 | require 'spec_helper'
 2 | 
 3 | describe Response, 'performance' do
 4 |   before do
 5 |     @response = Response.new
 6 |     @response.body = ''
 7 |   end
 8 |   
 9 |   it "should be fast" do
10 |     @response.body << <<-EOS
11 | Dir listing
12 | 

Listing stuff

    13 | #{'
  • Hi!
  • ' * 100} 14 |
15 | EOS 16 | 17 | expect { @response.each { |l| l } }.to be_faster_then(0.00011) 18 | end 19 | end -------------------------------------------------------------------------------- /spec/perf/server_perf_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Server, 'performance' do 4 | before do 5 | start_server do |env| 6 | body = env.inspect + env['rack.input'].read 7 | [200, { 'content-length' => body.size.to_s }, body] 8 | end 9 | end 10 | 11 | it "should handle GET in less then #{get_request_time = 0.0045} RubySecond" do 12 | expect { get('/') }.to be_faster_then(get_request_time) 13 | end 14 | 15 | it "should handle POST in less then #{post_request_time = 0.007} RubySecond" do 16 | expect { post('/', :file => 'X' * 1000) }.to be_faster_then(post_request_time) 17 | end 18 | 19 | after do 20 | stop_server 21 | end 22 | end 23 | 24 | describe Server, 'UNIX socket performance' do 25 | before do 26 | start_server('/tmp/thin_test.sock') do |env| 27 | body = env.inspect + env['rack.input'].read 28 | [200, { 'content-length' => body.size.to_s }, body] 29 | end 30 | end 31 | 32 | it "should handle GET in less then #{get_request_time = 0.002} RubySecond" do 33 | expect { get('/') }.to be_faster_then(get_request_time) 34 | end 35 | 36 | after do 37 | stop_server 38 | end 39 | end -------------------------------------------------------------------------------- /spec/rack/loader_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Rack::Adapter do 4 | before do 5 | @config_ru_path = File.dirname(__FILE__) + '/../../example' 6 | @rails_path = File.dirname(__FILE__) + '/../rails_app' 7 | end 8 | 9 | it "should load Rack app from config" do 10 | expect(Rack::Adapter.load(@config_ru_path + '/config.ru').class).to eq(Proc) 11 | end 12 | 13 | it "should guess Rack app from dir" do 14 | expect(Rack::Adapter.guess(@config_ru_path)).to eq(:rack) 15 | end 16 | 17 | it "should guess rails app from dir" do 18 | expect(Rack::Adapter.guess(@rails_path)).to eq(:rails) 19 | end 20 | 21 | it "should return nil when can't guess from dir" do 22 | expect { Rack::Adapter.guess('.') }.to raise_error(Rack::AdapterNotFound) 23 | end 24 | 25 | it "should load Rack adapter" do 26 | expect(Rack::Adapter.for(:rack, :chdir => @config_ru_path).class).to eq(Proc) 27 | end 28 | 29 | it "should load Rails adapter" do 30 | expect(Rack::Adapter::Rails).to receive(:new) 31 | Rack::Adapter.for(:rails, :chdir => @rails_path) 32 | end 33 | 34 | it "should load File adapter" do 35 | expect(Rack::Files).to receive(:new) 36 | Rack::Adapter.for(:file) 37 | end 38 | 39 | it "should raise error when adapter can't be found" do 40 | expect { Rack::Adapter.for(:fart, {}) }.to raise_error(Rack::AdapterNotFound) 41 | end 42 | end -------------------------------------------------------------------------------- /spec/rack/rails_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rack/mock' 3 | 4 | begin 5 | gem 'rails', '= 2.0.2' # We could freeze Rails in the rails_app dir to remove this 6 | 7 | describe Rack::Adapter::Rails do 8 | before do 9 | @rails_app_path = File.dirname(__FILE__) + '/../rails_app' 10 | @request = Rack::MockRequest.new(Rack::Adapter::Rails.new(:root => @rails_app_path)) 11 | end 12 | 13 | it "should handle simple GET request" do 14 | res = @request.get("/simple", :lint => true) 15 | 16 | expect(res).to be_ok 17 | expect(res["Content-Type"]).to include("text/html") 18 | 19 | expect(res.body).to include('Simple#index') 20 | end 21 | 22 | it "should handle POST parameters" do 23 | data = "foo=bar" 24 | res = @request.post("/simple/post_form", :input => data, 'CONTENT_LENGTH' => data.size.to_s, :lint => true) 25 | 26 | expect(res).to be_ok 27 | expect(res["Content-Type"]).to include("text/html") 28 | expect(res["Content-Length"]).not_to be_nil 29 | 30 | expect(res.body).to include('foo: bar') 31 | end 32 | 33 | it "should serve static files" do 34 | res = @request.get("/index.html", :lint => true) 35 | 36 | expect(res).to be_ok 37 | expect(res["Content-Type"]).to include("text/html") 38 | end 39 | 40 | it "should serve root with index.html if present" do 41 | res = @request.get("/", :lint => true) 42 | 43 | expect(res).to be_ok 44 | expect(res["Content-Length"].to_i).to eq(File.size(@rails_app_path + '/public/index.html')) 45 | end 46 | 47 | it "should serve page cache if present" do 48 | res = @request.get("/simple/cached?value=cached", :lint => true) 49 | 50 | expect(res).to be_ok 51 | expect(res.body).to eq('cached') 52 | 53 | res = @request.get("/simple/cached?value=notcached") 54 | 55 | expect(res).to be_ok 56 | expect(res.body).to eq('cached') 57 | end 58 | 59 | it "should not serve page cache on POST request" do 60 | res = @request.get("/simple/cached?value=cached", :lint => true) 61 | 62 | expect(res).to be_ok 63 | expect(res.body).to eq('cached') 64 | 65 | res = @request.post("/simple/cached?value=notcached") 66 | 67 | expect(res).to be_ok 68 | expect(res.body).to eq('notcached') 69 | end 70 | 71 | it "handles multiple cookies" do 72 | res = @request.get('/simple/set_cookie?name=a&value=1', :lint => true) 73 | 74 | expect(res).to be_ok 75 | expect(res.original_headers['Set-Cookie'].size).to eq(2) 76 | expect(res.original_headers['Set-Cookie'].first).to include('a=1; path=/') 77 | expect(res.original_headers['Set-Cookie'].last).to include('_rails_app_session') 78 | end 79 | 80 | after do 81 | FileUtils.rm_rf @rails_app_path + '/public/simple' 82 | end 83 | end 84 | 85 | describe Rack::Adapter::Rails, 'with prefix' do 86 | before do 87 | @rails_app_path = File.dirname(__FILE__) + '/../rails_app' 88 | @prefix = '/nowhere' 89 | @request = Rack::MockRequest.new( 90 | Rack::URLMap.new( 91 | @prefix => Rack::Adapter::Rails.new(:root => @rails_app_path, :prefix => @prefix))) 92 | end 93 | 94 | it "should handle simple GET request" do 95 | res = @request.get("#{@prefix}/simple", :lint => true) 96 | 97 | expect(res).to be_ok 98 | expect(res["Content-Type"]).to include("text/html") 99 | 100 | expect(res.body).to include('Simple#index') 101 | end 102 | end 103 | 104 | rescue Gem::LoadError 105 | warn 'Rails 2.0.2 is required to run the Rails adapter specs' 106 | end 107 | 108 | module RailsMock 109 | module VERSION 110 | MAJOR = 0 111 | MINOR = 0 112 | TINY = 0 113 | end 114 | end 115 | 116 | describe Rack::Adapter::Rails, "Adapter version" do 117 | before do 118 | unless defined?(::Rails) 119 | ::Rails = RailsMock 120 | end 121 | end 122 | 123 | it "should use Rack based adapter when Rails = 2.2.3" do 124 | with_rails_version(2, 2, 3) do 125 | expect(Rack::Adapter::Rails).to be_rack_based 126 | end 127 | end 128 | 129 | it "should not use Rack based adapter when Rails < 2.2.3" do 130 | with_rails_version(2, 2, 2) do 131 | expect(Rack::Adapter::Rails).not_to be_rack_based 132 | end 133 | end 134 | 135 | it "should not use Rack based adapter when Rails = 1.2.3" do 136 | with_rails_version(1, 2, 2) do 137 | expect(Rack::Adapter::Rails).not_to be_rack_based 138 | end 139 | end 140 | 141 | it "should use Rack based adapter when Rails = 3.0.0" do 142 | with_rails_version(3, 0, 0) do 143 | expect(Rack::Adapter::Rails).to be_rack_based 144 | end 145 | end 146 | 147 | def with_rails_version(major, minor, tiny) 148 | old_major = ::Rails::VERSION::MAJOR 149 | old_minor = ::Rails::VERSION::MINOR 150 | old_tiny = ::Rails::VERSION::TINY 151 | 152 | silence_warnings do 153 | ::Rails::VERSION.const_set :MAJOR, major 154 | ::Rails::VERSION.const_set :MINOR, minor 155 | ::Rails::VERSION.const_set :TINY, tiny 156 | end 157 | 158 | yield 159 | 160 | silence_warnings do 161 | ::Rails::VERSION.const_set :MAJOR, old_major 162 | ::Rails::VERSION.const_set :MINOR, old_minor 163 | ::Rails::VERSION.const_set :TINY, old_tiny 164 | end 165 | end 166 | 167 | def silence_warnings 168 | old_verbose, $VERBOSE = $VERBOSE, nil 169 | yield 170 | ensure 171 | $VERBOSE = old_verbose 172 | end unless method_defined?(:silence_warnings) 173 | end 174 | -------------------------------------------------------------------------------- /spec/rails_app/app/controllers/application.rb: -------------------------------------------------------------------------------- 1 | # Filters added to this controller apply to all controllers in the application. 2 | # Likewise, all the methods added will be available for all controllers. 3 | 4 | class ApplicationController < ActionController::Base 5 | helper :all # include all helpers, all the time 6 | 7 | # See ActionController::RequestForgeryProtection for details 8 | # Uncomment the :secret if you're not using the cookie session store 9 | # protect_from_forgery # :secret => 'a8af303b8dabf2d2d8f1a7912ac04d7d' 10 | end 11 | -------------------------------------------------------------------------------- /spec/rails_app/app/controllers/simple_controller.rb: -------------------------------------------------------------------------------- 1 | class SimpleController < ApplicationController 2 | caches_page :cached 3 | 4 | def index 5 | end 6 | 7 | def post_form 8 | render :text => params.to_yaml 9 | end 10 | 11 | def set_cookie 12 | cookies[params[:name]] = params[:value] if params[:name] 13 | render :text => cookies.to_yaml 14 | end 15 | 16 | def cached 17 | render :text => params[:value] 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/rails_app/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | # Methods added to this helper will be available to all templates in the application. 2 | module ApplicationHelper 3 | end 4 | -------------------------------------------------------------------------------- /spec/rails_app/app/views/simple/index.html.erb: -------------------------------------------------------------------------------- 1 |

Simple#index

2 | 3 |

ENV

4 | <%= request.env.to_yaml %> 5 | 6 |

Cookies

7 | <%= request.cookies.to_yaml %> 8 | 9 |

Params

10 | <%= params.to_yaml %> 11 | 12 | <% form_tag '/simple' do %> 13 | <%= text_field_tag :a %> 14 | <%= submit_tag 'Submit' %> 15 | <% end %> -------------------------------------------------------------------------------- /spec/rails_app/config/boot.rb: -------------------------------------------------------------------------------- 1 | # Don't change this file! 2 | # Configure your app in config/environment.rb and config/environments/*.rb 3 | 4 | RAILS_ROOT = "#{File.dirname(__FILE__)}/.." unless defined?(RAILS_ROOT) 5 | 6 | module Rails 7 | class << self 8 | def boot! 9 | unless booted? 10 | preinitialize 11 | pick_boot.run 12 | end 13 | end 14 | 15 | def booted? 16 | defined? Rails::Initializer 17 | end 18 | 19 | def pick_boot 20 | (vendor_rails? ? VendorBoot : GemBoot).new 21 | end 22 | 23 | def vendor_rails? 24 | File.exist?("#{RAILS_ROOT}/vendor/rails") 25 | end 26 | 27 | # FIXME : Ruby 1.9 28 | def preinitialize 29 | load(preinitializer_path) if File.exist?(preinitializer_path) 30 | end 31 | 32 | def preinitializer_path 33 | "#{RAILS_ROOT}/config/preinitializer.rb" 34 | end 35 | end 36 | 37 | class Boot 38 | def run 39 | load_initializer 40 | Rails::Initializer.run(:set_load_path) 41 | end 42 | end 43 | 44 | class VendorBoot < Boot 45 | def load_initializer 46 | require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer" 47 | end 48 | end 49 | 50 | class GemBoot < Boot 51 | def load_initializer 52 | self.class.load_rubygems 53 | load_rails_gem 54 | require 'initializer' 55 | end 56 | 57 | def load_rails_gem 58 | if version = self.class.gem_version 59 | gem 'rails', version 60 | else 61 | gem 'rails' 62 | end 63 | rescue Gem::LoadError => load_error 64 | $stderr.puts %(Missing the Rails #{version} gem. Please `gem install -v=#{version} rails`, update your RAILS_GEM_VERSION setting in config/environment.rb for the Rails version you do have installed, or comment out RAILS_GEM_VERSION to use the latest version installed.) 65 | exit 1 66 | end 67 | 68 | class << self 69 | def rubygems_version 70 | Gem::RubyGemsVersion if defined? Gem::RubyGemsVersion 71 | end 72 | 73 | def gem_version 74 | if defined? RAILS_GEM_VERSION 75 | RAILS_GEM_VERSION 76 | elsif ENV.include?('RAILS_GEM_VERSION') 77 | ENV['RAILS_GEM_VERSION'] 78 | else 79 | parse_gem_version(read_environment_rb) 80 | end 81 | end 82 | 83 | def load_rubygems 84 | require 'rubygems' 85 | 86 | unless rubygems_version >= '0.9.4' 87 | $stderr.puts %(Rails requires RubyGems >= 0.9.4 (you have #{rubygems_version}). Please `gem update --system` and try again.) 88 | exit 1 89 | end 90 | 91 | rescue LoadError 92 | $stderr.puts %(Rails requires RubyGems >= 0.9.4. Please install RubyGems and try again: http://rubygems.rubyforge.org) 93 | exit 1 94 | end 95 | 96 | def parse_gem_version(text) 97 | $1 if text =~ /^[^#]*RAILS_GEM_VERSION\s*=\s*["']([!~<>=]*\s*[\d.]+)["']/ 98 | end 99 | 100 | private 101 | def read_environment_rb 102 | File.read("#{RAILS_ROOT}/config/environment.rb") 103 | end 104 | end 105 | end 106 | end 107 | 108 | # All that for this: 109 | Rails.boot! 110 | -------------------------------------------------------------------------------- /spec/rails_app/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file 2 | 3 | # Uncomment below to force Rails into production mode when 4 | # you don't control web/app server and can't set it the proper way 5 | # ENV['RAILS_ENV'] ||= 'production' 6 | 7 | # Specifies gem version of Rails to use when vendor/rails is not present 8 | RAILS_GEM_VERSION = '2.0.2' unless defined? RAILS_GEM_VERSION 9 | 10 | # Bootstrap the Rails environment, frameworks, and default configuration 11 | require File.join(File.dirname(__FILE__), 'boot') 12 | 13 | Rails::Initializer.run do |config| 14 | # Settings in config/environments/* take precedence over those specified here. 15 | # Application configuration should go into files in config/initializers 16 | # -- all .rb files in that directory are automatically loaded. 17 | # See Rails::Configuration for more options. 18 | 19 | # Skip frameworks you're not going to use (only works if using vendor/rails). 20 | # To use Rails without a database, you must remove the Active Record framework 21 | config.frameworks -= [ :active_record, :active_resource, :action_mailer ] 22 | 23 | # Only load the plugins named here, in the order given. By default, all plugins 24 | # in vendor/plugins are loaded in alphabetical order. 25 | # :all can be used as a placeholder for all plugins not explicitly named 26 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ] 27 | 28 | # Add additional load paths for your own custom dirs 29 | # config.load_paths += %W( #{RAILS_ROOT}/extras ) 30 | 31 | # No need for log files 32 | config.logger = Logger.new(nil) 33 | 34 | # Force all environments to use the same logger level 35 | # (by default production uses :info, the others :debug) 36 | # config.log_level = :debug 37 | 38 | # Your secret key for verifying cookie session data integrity. 39 | # If you change this key, all old sessions will become invalid! 40 | # Make sure the secret is at least 30 characters and all random, 41 | # no regular words or you'll be exposed to dictionary attacks. 42 | config.action_controller.session = { 43 | :session_key => '_rails_app_session', 44 | :secret => 'cb7141365b4443eff37e7122473e704ceae95146a4028930b21300965fe6abec51e3e93b2670a914b3b65d06058b81aadfe6b240d63e7d7713db044b42a6e1c1' 45 | } 46 | 47 | config.action_controller.allow_forgery_protection = false 48 | 49 | # Use the database for sessions instead of the cookie-based default, 50 | # which shouldn't be used to store highly confidential information 51 | # (create the session table with 'rake db:sessions:create') 52 | # config.action_controller.session_store = :active_record_store 53 | 54 | # Use SQL instead of Active Record's schema dumper when creating the test database. 55 | # This is necessary if your schema can't be completely dumped by the schema dumper, 56 | # like if you have constraints or database-specific column types 57 | # config.active_record.schema_format = :sql 58 | 59 | # Activate observers that should always be running 60 | # config.active_record.observers = :cacher, :garbage_collector 61 | 62 | # Make Active Record use UTC-base instead of local time 63 | # config.active_record.default_timezone = :utc 64 | end -------------------------------------------------------------------------------- /spec/rails_app/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | # Settings specified here will take precedence over those in config/environment.rb 2 | 3 | # In the development environment your application's code is reloaded on 4 | # every request. This slows down response time but is perfect for development 5 | # since you don't have to restart the webserver when you make code changes. 6 | config.cache_classes = false 7 | 8 | # Log error messages when you accidentally call methods on nil. 9 | config.whiny_nils = true 10 | 11 | # Show full error reports and disable caching 12 | config.action_controller.consider_all_requests_local = true 13 | config.action_view.debug_rjs = true 14 | config.action_controller.perform_caching = true 15 | config.action_view.cache_template_extensions = false 16 | 17 | # Don't care if the mailer can't send 18 | config.action_mailer.raise_delivery_errors = false -------------------------------------------------------------------------------- /spec/rails_app/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | # Settings specified here will take precedence over those in config/environment.rb 2 | 3 | # The production environment is meant for finished, "live" apps. 4 | # Code is not reloaded between requests 5 | config.cache_classes = true 6 | 7 | # Use a different logger for distributed setups 8 | # config.logger = SyslogLogger.new 9 | 10 | # Full error reports are disabled and caching is turned on 11 | config.action_controller.consider_all_requests_local = false 12 | config.action_controller.perform_caching = true 13 | config.action_view.cache_template_loading = true 14 | 15 | # Enable serving of images, stylesheets, and javascripts from an asset server 16 | # config.action_controller.asset_host = "http://assets.example.com" 17 | 18 | # Disable delivery errors, bad email addresses will be ignored 19 | # config.action_mailer.raise_delivery_errors = false 20 | -------------------------------------------------------------------------------- /spec/rails_app/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | # Settings specified here will take precedence over those in config/environment.rb 2 | 3 | # The test environment is used exclusively to run your application's 4 | # test suite. You never need to work with it otherwise. Remember that 5 | # your test database is "scratch space" for the test suite and is wiped 6 | # and recreated between test runs. Don't rely on the data there! 7 | config.cache_classes = true 8 | 9 | # Log error messages when you accidentally call methods on nil. 10 | config.whiny_nils = true 11 | 12 | # Show full error reports and disable caching 13 | config.action_controller.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Disable request forgery protection in test environment 17 | config.action_controller.allow_forgery_protection = false 18 | 19 | # Tell ActionMailer not to deliver emails to the real world. 20 | # The :test delivery method accumulates sent emails in the 21 | # ActionMailer::Base.deliveries array. 22 | config.action_mailer.delivery_method = :test 23 | -------------------------------------------------------------------------------- /spec/rails_app/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format 4 | # (all these examples are active by default): 5 | # Inflector.inflections do |inflect| 6 | # inflect.plural /^(ox)$/i, '\1en' 7 | # inflect.singular /^(ox)en/i, '\1' 8 | # inflect.irregular 'person', 'people' 9 | # inflect.uncountable %w( fish sheep ) 10 | # end 11 | -------------------------------------------------------------------------------- /spec/rails_app/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | # Mime::Type.register_alias "text/html", :iphone 6 | -------------------------------------------------------------------------------- /spec/rails_app/config/routes.rb: -------------------------------------------------------------------------------- 1 | ActionController::Routing::Routes.draw do |map| 2 | # The priority is based upon order of creation: first created -> highest priority. 3 | 4 | # Sample of regular route: 5 | # map.connect 'products/:id', :controller => 'catalog', :action => 'view' 6 | # Keep in mind you can assign values other than :controller and :action 7 | 8 | # Sample of named route: 9 | # map.purchase 'products/:id/purchase', :controller => 'catalog', :action => 'purchase' 10 | # This route can be invoked with purchase_url(:id => product.id) 11 | 12 | # Sample resource route (maps HTTP verbs to controller actions automatically): 13 | # map.resources :products 14 | 15 | # Sample resource route with options: 16 | # map.resources :products, :member => { :short => :get, :toggle => :post }, :collection => { :sold => :get } 17 | 18 | # Sample resource route with sub-resources: 19 | # map.resources :products, :has_many => [ :comments, :sales ], :has_one => :seller 20 | 21 | # Sample resource route within a namespace: 22 | # map.namespace :admin do |admin| 23 | # # Directs /admin/products/* to Admin::ProductsController (app/controllers/admin/products_controller.rb) 24 | # admin.resources :products 25 | # end 26 | 27 | # You can have the root of your site routed with map.root -- just remember to delete public/index.html. 28 | # map.root :controller => "welcome" 29 | 30 | # See how all your routes lay out with "rake routes" 31 | 32 | # Install the default routes as the lowest priority. 33 | map.connect ':controller/:action/:id' 34 | map.connect ':controller/:action/:id.:format' 35 | end 36 | -------------------------------------------------------------------------------- /spec/rails_app/public/.htaccess: -------------------------------------------------------------------------------- 1 | # General Apache options 2 | AddHandler fastcgi-script .fcgi 3 | AddHandler cgi-script .cgi 4 | Options +FollowSymLinks +ExecCGI 5 | 6 | # If you don't want Rails to look in certain directories, 7 | # use the following rewrite rules so that Apache won't rewrite certain requests 8 | # 9 | # Example: 10 | # RewriteCond %{REQUEST_URI} ^/notrails.* 11 | # RewriteRule .* - [L] 12 | 13 | # Redirect all requests not available on the filesystem to Rails 14 | # By default the cgi dispatcher is used which is very slow 15 | # 16 | # For better performance replace the dispatcher with the fastcgi one 17 | # 18 | # Example: 19 | # RewriteRule ^(.*)$ dispatch.fcgi [QSA,L] 20 | RewriteEngine On 21 | 22 | # If your Rails application is accessed via an Alias directive, 23 | # then you MUST also set the RewriteBase in this htaccess file. 24 | # 25 | # Example: 26 | # Alias /myrailsapp /path/to/myrailsapp/public 27 | # RewriteBase /myrailsapp 28 | 29 | RewriteRule ^$ index.html [QSA] 30 | RewriteRule ^([^.]+)$ $1.html [QSA] 31 | RewriteCond %{REQUEST_FILENAME} !-f 32 | RewriteRule ^(.*)$ dispatch.cgi [QSA,L] 33 | 34 | # In case Rails experiences terminal errors 35 | # Instead of displaying this message you can supply a file here which will be rendered instead 36 | # 37 | # Example: 38 | # ErrorDocument 500 /500.html 39 | 40 | ErrorDocument 500 "

Application error

Rails application failed to start properly" 41 | -------------------------------------------------------------------------------- /spec/rails_app/public/404.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | The page you were looking for doesn't exist (404) 9 | 21 | 22 | 23 | 24 | 25 |
26 |

The page you were looking for doesn't exist.

27 |

You may have mistyped the address or the page may have moved.

28 |
29 | 30 | -------------------------------------------------------------------------------- /spec/rails_app/public/422.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | The change you wanted was rejected (422) 9 | 21 | 22 | 23 | 24 | 25 |
26 |

The change you wanted was rejected.

27 |

Maybe you tried to change something you didn't have access to.

28 |
29 | 30 | -------------------------------------------------------------------------------- /spec/rails_app/public/500.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | We're sorry, but something went wrong (500) 9 | 21 | 22 | 23 | 24 | 25 |
26 |

We're sorry, but something went wrong.

27 |

We've been notified about this issue and we'll take a look at it shortly.

28 |
29 | 30 | -------------------------------------------------------------------------------- /spec/rails_app/public/dispatch.cgi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT) 4 | 5 | # If you're using RubyGems and mod_ruby, this require should be changed to an absolute path one, like: 6 | # "/usr/local/lib/ruby/gems/1.8/gems/rails-0.8.0/lib/dispatcher" -- otherwise performance is severely impaired 7 | require "dispatcher" 8 | 9 | ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } if defined?(Apache::RubyRun) 10 | Dispatcher.dispatch 11 | -------------------------------------------------------------------------------- /spec/rails_app/public/dispatch.fcgi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # You may specify the path to the FastCGI crash log (a log of unhandled 4 | # exceptions which forced the FastCGI instance to exit, great for debugging) 5 | # and the number of requests to process before running garbage collection. 6 | # 7 | # By default, the FastCGI crash log is RAILS_ROOT/log/fastcgi.crash.log 8 | # and the GC period is nil (turned off). A reasonable number of requests 9 | # could range from 10-100 depending on the memory footprint of your app. 10 | # 11 | # Example: 12 | # # Default log path, normal GC behavior. 13 | # RailsFCGIHandler.process! 14 | # 15 | # # Default log path, 50 requests between GC. 16 | # RailsFCGIHandler.process! nil, 50 17 | # 18 | # # Custom log path, normal GC behavior. 19 | # RailsFCGIHandler.process! '/var/log/myapp_fcgi_crash.log' 20 | # 21 | require File.dirname(__FILE__) + "/../config/environment" 22 | require 'fcgi_handler' 23 | 24 | RailsFCGIHandler.process! 25 | -------------------------------------------------------------------------------- /spec/rails_app/public/dispatch.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT) 4 | 5 | # If you're using RubyGems and mod_ruby, this require should be changed to an absolute path one, like: 6 | # "/usr/local/lib/ruby/gems/1.8/gems/rails-0.8.0/lib/dispatcher" -- otherwise performance is severely impaired 7 | require "dispatcher" 8 | 9 | ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } if defined?(Apache::RubyRun) 10 | Dispatcher.dispatch 11 | -------------------------------------------------------------------------------- /spec/rails_app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macournoyer/thin/de6b6188f1f1cd7645948b266e3861f82c9c58a1/spec/rails_app/public/favicon.ico -------------------------------------------------------------------------------- /spec/rails_app/public/images/rails.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macournoyer/thin/de6b6188f1f1cd7645948b266e3861f82c9c58a1/spec/rails_app/public/images/rails.png -------------------------------------------------------------------------------- /spec/rails_app/public/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // Place your application-specific JavaScript functions and classes here 2 | // This file is automatically included by javascript_include_tag :defaults 3 | -------------------------------------------------------------------------------- /spec/rails_app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-Agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /spec/rails_app/script/about: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../config/boot' 3 | require 'commands/about' 4 | -------------------------------------------------------------------------------- /spec/rails_app/script/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../config/boot' 3 | require 'commands/console' 4 | -------------------------------------------------------------------------------- /spec/rails_app/script/destroy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../config/boot' 3 | require 'commands/destroy' 4 | -------------------------------------------------------------------------------- /spec/rails_app/script/generate: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../config/boot' 3 | require 'commands/generate' 4 | -------------------------------------------------------------------------------- /spec/rails_app/script/performance/benchmarker: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../../config/boot' 3 | require 'commands/performance/benchmarker' 4 | -------------------------------------------------------------------------------- /spec/rails_app/script/performance/profiler: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../../config/boot' 3 | require 'commands/performance/profiler' 4 | -------------------------------------------------------------------------------- /spec/rails_app/script/performance/request: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../../config/boot' 3 | require 'commands/performance/request' 4 | -------------------------------------------------------------------------------- /spec/rails_app/script/plugin: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../config/boot' 3 | require 'commands/plugin' 4 | -------------------------------------------------------------------------------- /spec/rails_app/script/process/inspector: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../../config/boot' 3 | require 'commands/process/inspector' 4 | -------------------------------------------------------------------------------- /spec/rails_app/script/process/reaper: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../../config/boot' 3 | require 'commands/process/reaper' 4 | -------------------------------------------------------------------------------- /spec/rails_app/script/process/spawner: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../../config/boot' 3 | require 'commands/process/spawner' 4 | -------------------------------------------------------------------------------- /spec/rails_app/script/runner: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../config/boot' 3 | require 'commands/runner' 4 | -------------------------------------------------------------------------------- /spec/rails_app/script/server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../config/boot' 3 | require 'commands/server' 4 | -------------------------------------------------------------------------------- /spec/request/persistent_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Request, 'persistent' do 4 | before do 5 | @request = Request.new 6 | end 7 | 8 | it "should not assume that a persistent connection is maintained for HTTP version 1.0" do 9 | @request.env['thin.request_http_version'] = 'HTTP/1.0' 10 | expect(@request).not_to be_persistent 11 | end 12 | 13 | it "should assume that a persistent connection is maintained for HTTP version 1.0 when specified" do 14 | @request.env['thin.request_http_version'] = 'HTTP/1.0' 15 | @request.env['HTTP_CONNECTION'] = 'Keep-Alive' 16 | expect(@request).to be_persistent 17 | end 18 | 19 | it "should maintain a persistent connection for HTTP/1.1 client" do 20 | @request.env['thin.request_http_version'] = 'HTTP/1.1' 21 | @request.env['HTTP_CONNECTION'] = 'Keep-Alive' 22 | expect(@request).to be_persistent 23 | end 24 | 25 | it "should maintain a persistent connection for HTTP/1.1 client by default" do 26 | @request.env['thin.request_http_version'] = 'HTTP/1.1' 27 | expect(@request).to be_persistent 28 | end 29 | 30 | it "should not maintain a persistent connection for HTTP/1.1 client when Connection header include close" do 31 | @request.env['thin.request_http_version'] = 'HTTP/1.1' 32 | @request.env['HTTP_CONNECTION'] = 'close' 33 | expect(@request).not_to be_persistent 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/request/processing_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Request, 'processing' do 4 | it 'should parse in chunks' do 5 | request = Request.new 6 | expect(request.parse("POST / HTTP/1.1\r\n")).to be_falsey 7 | expect(request.parse("Host: localhost\r\n")).to be_falsey 8 | expect(request.parse("Content-Length: 9\r\n")).to be_falsey 9 | expect(request.parse("\r\nvery ")).to be_falsey 10 | expect(request.parse("cool")).to be_truthy 11 | 12 | expect(request.env['CONTENT_LENGTH']).to eq('9') 13 | expect(request.body.read).to eq('very cool') 14 | expect(request).to validate_with_lint 15 | end 16 | 17 | it "should move body to tempfile when too big" do 18 | len = Request::MAX_BODY + 2 19 | request = Request.new 20 | request.parse("POST /postit HTTP/1.1\r\nContent-Length: #{len}\r\n\r\n#{'X' * (len/2)}") 21 | request.parse('X' * (len/2)) 22 | 23 | expect(request.body.size).to eq(len) 24 | expect(request).to be_finished 25 | expect(request.body.class).to eq(Tempfile) 26 | end 27 | 28 | it "should delete body tempfile when closing" do 29 | body = 'X' * (Request::MAX_BODY + 1) 30 | 31 | request = Request.new 32 | request.parse("POST /postit HTTP/1.1\r\n") 33 | request.parse("Content-Length: #{body.size}\r\n\r\n") 34 | request.parse(body) 35 | 36 | expect(request.body.path).not_to be_nil 37 | request.close 38 | expect(request.body.path).to be_nil 39 | end 40 | 41 | it "should close body tempfile when closing" do 42 | body = 'X' * (Request::MAX_BODY + 1) 43 | 44 | request = Request.new 45 | request.parse("POST /postit HTTP/1.1\r\n") 46 | request.parse("Content-Length: #{body.size}\r\n\r\n") 47 | request.parse(body) 48 | 49 | expect(request.body.closed?).to be_falsey 50 | request.close 51 | expect(request.body.closed?).to be_truthy 52 | end 53 | 54 | it "should raise error when header is too big" do 55 | big_headers = "X-Test: X\r\n" * (1024 * (80 + 32)) 56 | expect { R("GET / HTTP/1.1\r\n#{big_headers}\r\n") }.to raise_error(InvalidRequest) 57 | end 58 | 59 | it "should set body external encoding to ASCII_8BIT" do 60 | pending("Ruby 1.9 compatible implementations only") unless StringIO.instance_methods.include? :external_encoding 61 | expect(Request.new.body.external_encoding).to eq(Encoding::ASCII_8BIT) 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /spec/response_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Response do 4 | before do 5 | @response = Response.new 6 | @response.headers['content-type'] = 'text/html' 7 | @response.headers['content-length'] = '0' 8 | @response.body = '' 9 | end 10 | 11 | it 'should output headers' do 12 | expect(@response.headers_output).to include("content-type: text/html", "content-length: 0", "connection: close") 13 | end 14 | 15 | it 'should include server name header' do 16 | expect(@response.headers_output).to include("server: thin") 17 | end 18 | 19 | it 'should output head' do 20 | expect(@response.head).to include("HTTP/1.1 200 OK", "content-type: text/html", "content-length: 0", 21 | "connection: close", "\r\n\r\n") 22 | end 23 | 24 | it 'should allow duplicates in headers' do 25 | @response.headers['Set-Cookie'] = 'mium=7' 26 | @response.headers['Set-Cookie'] = 'hi=there' 27 | 28 | expect(@response.head).to include("Set-Cookie: mium=7", "Set-Cookie: hi=there") 29 | end 30 | 31 | it 'should parse simple header values' do 32 | @response.headers = { 33 | 'Host' => 'localhost' 34 | } 35 | 36 | expect(@response.head).to include("Host: localhost") 37 | end 38 | 39 | it 'should parse multiline header values in several headers' do 40 | @response.headers = { 41 | 'Set-Cookie' => "mium=7\nhi=there" 42 | } 43 | 44 | expect(@response.head).to include("Set-Cookie: mium=7", "Set-Cookie: hi=there") 45 | end 46 | 47 | it 'should ignore nil headers' do 48 | @response.headers = nil 49 | @response.headers = { 'Host' => 'localhost' } 50 | @response.headers = { 'Set-Cookie' => nil } 51 | expect(@response.head).to include('Host: localhost') 52 | end 53 | 54 | it 'should output body' do 55 | @response.body = ['', ''] 56 | 57 | out = '' 58 | @response.each { |l| out << l } 59 | expect(out).to include("\r\n\r\n") 60 | end 61 | 62 | it 'should output String body' do 63 | @response.body = '' 64 | 65 | out = '' 66 | @response.each { |l| out << l } 67 | expect(out).to include("\r\n\r\n") 68 | end 69 | 70 | it "should not be persistent by default" do 71 | expect(@response).not_to be_persistent 72 | end 73 | 74 | it "should not be persistent when no content-length" do 75 | @response = Response.new 76 | @response.headers = {'content-type' => 'text/html'} 77 | @response.body = '' 78 | 79 | @response.persistent! 80 | expect(@response).not_to be_persistent 81 | end 82 | 83 | it "should ignore content-length case" do 84 | @response = Response.new 85 | @response.headers = {'content-type' => 'text/html', 'content-length' => '0'} 86 | @response.body = '' 87 | 88 | @response.persistent! 89 | expect(@response).to be_persistent 90 | end 91 | 92 | it "should be persistent when the status code implies it should stay open" do 93 | @response = Response.new 94 | @response.status = 100 95 | # "There are no required headers for this class of status code" -- HTTP spec 10.1 96 | @response.body = '' 97 | 98 | # Specifying it as persistent in the code is NOT required 99 | # @response.persistent! 100 | expect(@response).to be_persistent 101 | end 102 | 103 | it "should be persistent when specified" do 104 | @response.persistent! 105 | expect(@response).to be_persistent 106 | end 107 | 108 | it "should be closeable" do 109 | @response.close 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /spec/runner_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Runner do 4 | it "should parse options" do 5 | runner = Runner.new(%w(start --pid test.pid --port 5000 -o 3000)) 6 | 7 | expect(runner.options[:pid]).to eq('test.pid') 8 | expect(runner.options[:port]).to eq(5000) 9 | expect(runner.options[:only]).to eq(3000) 10 | end 11 | 12 | it "should parse specified command" do 13 | expect(Runner.new(%w(start)).command).to eq('start') 14 | expect(Runner.new(%w(stop)).command).to eq('stop') 15 | expect(Runner.new(%w(restart)).command).to eq('restart') 16 | end 17 | 18 | it "should abort on unknow command" do 19 | runner = Runner.new(%w(poop)) 20 | 21 | expect(runner).to receive(:abort) 22 | runner.run! 23 | end 24 | 25 | it "should exit on empty command" do 26 | runner = Runner.new([]) 27 | 28 | expect(runner).to receive(:exit).with(1) 29 | 30 | silence_stream(STDOUT) do 31 | runner.run! 32 | end 33 | end 34 | 35 | it "should use Controller when controlling a single server" do 36 | runner = Runner.new(%w(start)) 37 | 38 | controller = double('controller') 39 | expect(controller).to receive(:start) 40 | expect(Controllers::Controller).to receive(:new).and_return(controller) 41 | 42 | runner.run! 43 | end 44 | 45 | it "should use Cluster controller when controlling multiple servers" do 46 | runner = Runner.new(%w(start --servers 3)) 47 | 48 | controller = double('cluster') 49 | expect(controller).to receive(:start) 50 | expect(Controllers::Cluster).to receive(:new).and_return(controller) 51 | 52 | runner.run! 53 | end 54 | 55 | it "should default to single server controller" do 56 | expect(Runner.new(%w(start))).not_to be_a_cluster 57 | end 58 | 59 | it "should consider as a cluster with :servers option" do 60 | expect(Runner.new(%w(start --servers 3))).to be_a_cluster 61 | end 62 | 63 | it "should consider as a cluster with :only option" do 64 | expect(Runner.new(%w(start --only 3000))).to be_a_cluster 65 | end 66 | 67 | it "should warn when require a rack config file" do 68 | runner = Runner.new(%w(start -r config.ru)) 69 | 70 | expect(runner).to receive(:warn).with(/WARNING:/) 71 | 72 | runner.run! rescue nil 73 | 74 | expect(runner.options[:rackup]).to eq('config.ru') 75 | end 76 | 77 | it "should require file" do 78 | runner = Runner.new(%w(start -r unexisting)) 79 | expect { runner.run! }.to raise_error(LoadError) 80 | end 81 | 82 | it "should remember requires" do 83 | runner = Runner.new(%w(start -r rubygems -r thin)) 84 | expect(runner.options[:require]).to eq(%w(rubygems thin)) 85 | end 86 | 87 | it "should remember debug options" do 88 | runner = Runner.new(%w(start -D -q -V)) 89 | expect(runner.options[:debug]).to be_truthy 90 | expect(runner.options[:quiet]).to be_truthy 91 | expect(runner.options[:trace]).to be_truthy 92 | end 93 | 94 | it "should default debug, silent and trace to false" do 95 | runner = Runner.new(%w(start)) 96 | expect(runner.options[:debug]).not_to be_truthy 97 | expect(runner.options[:quiet]).not_to be_truthy 98 | expect(runner.options[:trace]).not_to be_truthy 99 | end 100 | end 101 | 102 | describe Runner, 'with config file' do 103 | before :each do 104 | @runner = Runner.new(%w(start --config spec/configs/cluster.yml)) 105 | end 106 | 107 | it "should load options from file with :config option" do 108 | @runner.send :load_options_from_config_file! 109 | 110 | expect(@runner.options[:environment]).to eq('production') 111 | expect(@runner.options[:chdir]).to eq('spec/rails_app') 112 | expect(@runner.options[:port]).to eq(5000) 113 | expect(@runner.options[:servers]).to eq(3) 114 | end 115 | 116 | it "should load options from file using an ERB template" do 117 | @runner = Runner.new(%w(start --config spec/configs/with_erb.yml)) 118 | @runner.send :load_options_from_config_file! 119 | 120 | expect(@runner.options[:timeout]).to eq(30) 121 | expect(@runner.options[:port]).to eq(4000) 122 | expect(@runner.options[:environment]).to eq('production') 123 | end 124 | 125 | it "should change directory after loading config" do 126 | @orig_dir = Dir.pwd 127 | 128 | controller = double('controller') 129 | expect(controller).to receive(:respond_to?).with('start').and_return(true) 130 | expect(controller).to receive(:start) 131 | expect(Controllers::Cluster).to receive(:new).and_return(controller) 132 | expected_dir = File.expand_path('spec/rails_app') 133 | 134 | begin 135 | silence_stream(STDERR) do 136 | @runner.run! 137 | end 138 | 139 | expect(Dir.pwd).to eq(expected_dir) 140 | 141 | ensure 142 | # any other spec using relative paths should work as expected 143 | Dir.chdir(@orig_dir) 144 | end 145 | end 146 | end 147 | 148 | describe Runner, "service" do 149 | before do 150 | allow(Thin).to receive(:linux?) { true } 151 | 152 | @controller = double('service') 153 | allow(Controllers::Service).to receive(:new) { @controller } 154 | end 155 | 156 | it "should use Service controller when controlling all servers" do 157 | runner = Runner.new(%w(start --all)) 158 | 159 | expect(@controller).to receive(:start) 160 | 161 | runner.run! 162 | end 163 | 164 | it "should call install with arguments" do 165 | runner = Runner.new(%w(install /etc/cool)) 166 | 167 | expect(@controller).to receive(:install).with('/etc/cool') 168 | 169 | runner.run! 170 | end 171 | 172 | it "should call install without arguments" do 173 | runner = Runner.new(%w(install)) 174 | 175 | expect(@controller).to receive(:install).with(no_args) 176 | 177 | runner.run! 178 | end 179 | end 180 | -------------------------------------------------------------------------------- /spec/server/builder_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Server, 'app builder' do 4 | 5 | before :all do 6 | Logging.debug = false 7 | end 8 | 9 | it "should build app from constructor" do 10 | app = proc {} 11 | server = Server.new('0.0.0.0', 3000, app) 12 | 13 | expect(server.app).to eq(app) 14 | end 15 | 16 | it "should build app from builder block" do 17 | server = Server.new '0.0.0.0', 3000 do 18 | run(proc { |env| :works }) 19 | end 20 | 21 | expect(server.app.call({})).to eq(:works) 22 | end 23 | 24 | it "should use middlewares in builder block" do 25 | server = Server.new '0.0.0.0', 3000 do 26 | use Rack::ShowExceptions 27 | run(proc { |env| :works }) 28 | end 29 | 30 | expect(server.app.class).to eq(Rack::ShowExceptions) 31 | expect(server.app.call({})).to eq(:works) 32 | end 33 | 34 | it "should work with Rack url mapper" do 35 | server = Server.new '0.0.0.0', 3000 do 36 | map '/test' do 37 | run(proc { |env| [200, {}, 'Found /test'] }) 38 | end 39 | end 40 | 41 | default_env = { 'SCRIPT_NAME' => '' } 42 | 43 | expect(server.app.call(default_env.update('PATH_INFO' => '/'))[0]).to eq(404) 44 | 45 | status, headers, body = server.app.call(default_env.update('PATH_INFO' => '/test')) 46 | expect(status).to eq(200) 47 | expect(body).to eq('Found /test') 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/server/robustness_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Server, 'robustness' do 4 | before do 5 | start_server do |env| 6 | body = 'hello!' 7 | [200, { 'content-type' => 'text/html' }, body] 8 | end 9 | end 10 | 11 | it "should not crash when header too large" do 12 | 100.times do 13 | begin 14 | socket = TCPSocket.new(DEFAULT_TEST_ADDRESS, DEFAULT_TEST_PORT) 15 | socket.write("GET / HTTP/1.1\r\n") 16 | socket.write("Host: localhost\r\n") 17 | socket.write("Connection: close\r\n") 18 | 10000.times do 19 | socket.write("X-Foo: #{'x' * 100}\r\n") 20 | socket.flush 21 | end 22 | socket.write("\r\n") 23 | socket.read 24 | socket.close 25 | rescue Errno::EPIPE, Errno::ECONNRESET 26 | # Ignore. 27 | end 28 | end 29 | end 30 | 31 | after do 32 | stop_server 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/server/stopping_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | require 'timeout' 4 | 5 | describe Server, "stopping" do 6 | before do 7 | start_server do |env| 8 | [200, { 'content-type' => 'text/html' }, ['ok']] 9 | end 10 | @done = false 11 | end 12 | 13 | it "should wait for current requests before soft stopping" do 14 | socket = TCPSocket.new('0.0.0.0', 3333) 15 | socket.write("GET / HTTP/1.1") 16 | EventMachine.next_tick do 17 | @server.stop # Stop the server in the middle of a request 18 | socket.write("\r\n\r\n") 19 | @done = true 20 | end 21 | 22 | Timeout.timeout(2) do 23 | Thread.pass until @done 24 | end 25 | 26 | out = socket.read 27 | socket.close 28 | 29 | expect(out).not_to be_empty 30 | end 31 | 32 | it "should not accept new requests when soft stopping" do 33 | socket = TCPSocket.new('0.0.0.0', 3333) 34 | socket.write("GET / HTTP/1.1") 35 | @server.stop # Stop the server in the middle of a request 36 | 37 | EventMachine.next_tick do 38 | expect { get('/') }.to raise_error(Errno::ECONNRESET) 39 | end 40 | 41 | socket.close 42 | end 43 | 44 | it "should drop current requests when hard stopping" do 45 | socket = TCPSocket.new('0.0.0.0', 3333) 46 | socket.write("GET / HTTP/1.1") 47 | @server.stop! # Force stop the server in the middle of a request 48 | 49 | EventMachine.next_tick do 50 | expect(socket).to be_closed 51 | end 52 | end 53 | 54 | after do 55 | stop_server 56 | end 57 | end -------------------------------------------------------------------------------- /spec/server/swiftiply.yml: -------------------------------------------------------------------------------- 1 | cluster_address: 0.0.0.0 2 | cluster_port: 3333 3 | map: 4 | - incoming: 127.0.0.1 5 | outgoing: 127.0.0.1:5555 6 | default: true -------------------------------------------------------------------------------- /spec/server/swiftiply_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | if SWIFTIPLY_PATH.empty? 4 | warn "Ignoring Server on Swiftiply specs, gem install swiftiply to run" 5 | else 6 | describe Server, 'on Swiftiply' do 7 | before do 8 | @swiftiply = fork do 9 | exec "#{SWIFTIPLY_PATH} -c #{File.dirname(__FILE__)}/swiftiply.yml" 10 | end 11 | wait_for_socket('0.0.0.0', 3333) 12 | sleep 2 # HACK ooh boy, I wish I knew how to make those specs more stable... 13 | start_server('0.0.0.0', 5555, :backend => Backends::SwiftiplyClient, :wait_for_socket => false) do |env| 14 | body = env.inspect + env['rack.input'].read 15 | [200, { 'content-type' => 'text/html' }, body] 16 | end 17 | end 18 | 19 | it 'should GET from Net::HTTP' do 20 | expect(Net::HTTP.get(URI.parse("http://0.0.0.0:3333/?cthis"))).to include('cthis') 21 | end 22 | 23 | it 'should POST from Net::HTTP' do 24 | expect(Net::HTTP.post_form(URI.parse("http://0.0.0.0:3333/"), :arg => 'pirate').body).to include('arg=pirate') 25 | end 26 | 27 | after do 28 | stop_server 29 | Process.kill(9, @swiftiply) 30 | end 31 | end 32 | end -------------------------------------------------------------------------------- /spec/server/tcp_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Server, 'on TCP socket' do 4 | before do 5 | start_server do |env| 6 | body = env.inspect + env['rack.input'].read 7 | [200, { 'content-type' => 'text/html' }, body] 8 | end 9 | end 10 | 11 | it 'should GET from Net::HTTP' do 12 | expect(get('/?cthis')).to include('cthis') 13 | end 14 | 15 | it 'should GET from TCPSocket' do 16 | status, headers, body = parse_response(send_data("GET /?this HTTP/1.0\r\nConnection: close\r\n\r\n")) 17 | expect(status).to eq(200) 18 | expect(headers['content-type']).to eq('text/html') 19 | expect(headers['connection']).to eq('close') 20 | expect(body).to include('this') 21 | end 22 | 23 | it 'should return empty string on incomplete headers' do 24 | expect(send_data("GET /?this HTTP/1.1\r\nHost:")).to be_empty 25 | end 26 | 27 | it 'should return empty string on incorrect Content-Length' do 28 | expect(send_data("POST / HTTP/1.1\r\nContent-Length: 300\r\nConnection: close\r\n\r\naye")).to be_empty 29 | end 30 | 31 | it 'should POST from Net::HTTP' do 32 | expect(post('/', :arg => 'pirate')).to include('arg=pirate') 33 | end 34 | 35 | it 'should handle big POST' do 36 | big = 'X' * (20 * 1024) 37 | expect(post('/', :big => big)).to include(big) 38 | end 39 | 40 | it "should retrieve remote address" do 41 | expect(get('/')).to be =~ /"REMOTE_ADDR"\s*=>\s*"127.0.0.1"/ 42 | end 43 | 44 | after do 45 | stop_server 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/server/threaded_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Server, 'with threads' do 4 | before do 5 | @requests = 0 6 | start_server DEFAULT_TEST_ADDRESS, DEFAULT_TEST_PORT, :threaded => true do |env| 7 | sleep env['PATH_INFO'].delete('/').to_i 8 | @requests += 1 9 | [200, { 'content-type' => 'text/html' }, 'hi'] 10 | end 11 | end 12 | 13 | it "should process request" do 14 | expect(get('/')).not_to be_empty 15 | end 16 | 17 | it "should process requests when blocked" do 18 | slow_request = Thread.new { get('/3') } 19 | expect(get('/')).not_to be_empty 20 | expect(@requests).to eq(1) 21 | slow_request.kill 22 | end 23 | 24 | after do 25 | stop_server 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/server/unix_socket_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Server, "on UNIX domain socket" do 4 | before do 5 | start_server('/tmp/thin_test.sock') do |env| 6 | [200, { 'content-type' => 'text/html' }, [env.inspect]] 7 | end 8 | end 9 | 10 | it "should accept GET request" do 11 | expect(get("/?this")).to include('this') 12 | end 13 | 14 | it "should retreive remote address" do 15 | expect(get('/')).to be =~ /"REMOTE_ADDR"\s*=>\s*"127.0.0.1"/ 16 | end 17 | 18 | it "should remove socket file after server stops" do 19 | @server.stop! 20 | expect(File.exist?('/tmp/thin_test.sock')).to be_falsey 21 | end 22 | 23 | after do 24 | stop_server 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/server_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Server do 4 | before do 5 | @server = Server.new('0.0.0.0', 3000) 6 | end 7 | 8 | it "should set maximum_connections size" do 9 | @server.maximum_connections = 100 10 | @server.config 11 | expect(@server.maximum_connections).to eq(100) 12 | end 13 | 14 | it "should set lower maximum_connections size when too large" do 15 | # root users under Linux will not have a limitation on maximum 16 | # connections, so we cannot really run this test under that 17 | # condition. 18 | pending("only for non-root users") if Process.euid == 0 19 | maximum_connections = 1_000_000 20 | @server.maximum_connections = maximum_connections 21 | @server.config 22 | expect(@server.maximum_connections).to be <= maximum_connections 23 | end 24 | 25 | it "should default to non-threaded" do 26 | expect(@server).not_to be_threaded 27 | end 28 | 29 | it "should set backend to threaded" do 30 | @server.threaded = true 31 | expect(@server.backend).to be_threaded 32 | end 33 | 34 | it "should set the threadpool" do 35 | @server.threadpool_size = 10 36 | expect(@server.threadpool_size).to eq(10) 37 | end 38 | end 39 | 40 | describe Server, "initialization" do 41 | it "should set host and port" do 42 | server = Server.new('192.168.1.1', 8080) 43 | 44 | expect(server.host).to eq('192.168.1.1') 45 | expect(server.port).to eq(8080) 46 | end 47 | 48 | it "should set socket" do 49 | server = Server.new('/tmp/thin.sock') 50 | 51 | expect(server.socket).to eq('/tmp/thin.sock') 52 | end 53 | 54 | it "should set host, port and app" do 55 | app = proc {} 56 | server = Server.new('192.168.1.1', 8080, app) 57 | 58 | expect(server.host).not_to be_nil 59 | expect(server.app).to eq(app) 60 | end 61 | 62 | it "should set socket and app" do 63 | app = proc {} 64 | server = Server.new('/tmp/thin.sock', app) 65 | 66 | expect(server.socket).not_to be_nil 67 | expect(server.app).to eq(app) 68 | end 69 | 70 | it "should set socket, nil and app" do 71 | app = proc {} 72 | server = Server.new('/tmp/thin.sock', nil, app) 73 | 74 | expect(server.socket).not_to be_nil 75 | expect(server.app).to eq(app) 76 | end 77 | 78 | it "should set host, port and backend" do 79 | server = Server.new('192.168.1.1', 8080, :backend => Thin::Backends::SwiftiplyClient) 80 | 81 | expect(server.host).not_to be_nil 82 | expect(server.backend).to be_kind_of(Thin::Backends::SwiftiplyClient) 83 | end 84 | 85 | it "should set host, port, app and backend" do 86 | app = proc {} 87 | server = Server.new('192.168.1.1', 8080, app, :backend => Thin::Backends::SwiftiplyClient) 88 | 89 | expect(server.host).not_to be_nil 90 | expect(server.app).to eq(app) 91 | expect(server.backend).to be_kind_of(Thin::Backends::SwiftiplyClient) 92 | end 93 | 94 | it "should set port as string" do 95 | app = proc {} 96 | server = Server.new('192.168.1.1', '8080') 97 | 98 | expect(server.host).to eq('192.168.1.1') 99 | expect(server.port).to eq(8080) 100 | end 101 | 102 | it "should not register signals w/ :signals => false" do 103 | expect(Server).not_to receive(:setup_signals) 104 | Server.new(:signals => false) 105 | end 106 | end -------------------------------------------------------------------------------- /tasks/announce.rake: -------------------------------------------------------------------------------- 1 | require 'erb' 2 | 3 | MSG_TEMPLATE = File.dirname(__FILE__) + '/email.erb' 4 | SEND_TO = %w(thin-ruby@googlegroups.com ruby-talk@ruby-lang.org) 5 | 6 | desc 'Generate a template for the new version annoucement' 7 | task :ann do 8 | msg = ERB.new(File.read(MSG_TEMPLATE)).result(binding) 9 | 10 | body = < %w(site:upload rdoc:upload) 3 | 4 | desc 'Deploy on rubyforge' 5 | task :gem => %w(gem:upload_rubyforge deploy:site) 6 | end 7 | desc 'Deploy on all servers' 8 | task :deploy => "deploy:gem" 9 | 10 | def upload(file, to, options={}) 11 | sh %{ssh macournoyer@macournoyer.com "rm -rf code.macournoyer.com/#{to}"} if options[:replace] 12 | sh %{scp -rq #{file} macournoyer@macournoyer.com:code.macournoyer.com/#{to}} 13 | end 14 | -------------------------------------------------------------------------------- /tasks/email.erb: -------------------------------------------------------------------------------- 1 | Hey, 2 | 3 | Thin version <%= Thin::VERSION::STRING %> (codename <%= Thin::VERSION::CODENAME %>) is out! 4 | 5 | == What's new? 6 | 7 | <%= changelog %> 8 | 9 | == Get it! 10 | 11 | Install Thin from RubyGems: 12 | 13 | gem install thin 14 | 15 | == Contribute 16 | 17 | Site: http://code.macournoyer.com/thin/ 18 | Group: http://groups.google.com/group/thin-ruby/topics 19 | Issues: https://github.com/macournoyer/thin/issues 20 | Security issues: macournoyer+thinsecurity@gmail.com 21 | Code: http://github.com/macournoyer/thin 22 | IRC: #thin on freenode 23 | 24 | Thanks to all the people who contributed to Thin, EventMachine, Rack and Mongrel. 25 | 26 | Marc-Andre Cournoyer 27 | http://macournoyer.com/ -------------------------------------------------------------------------------- /tasks/ext.rake: -------------------------------------------------------------------------------- 1 | require 'rake/extensiontask' # from rake-compiler gem 2 | 3 | Rake::ExtensionTask.new('thin_parser', Thin::GemSpec) do |ext| 4 | # enable cross compilation (requires cross compile toolchain) 5 | ext.cross_compile = true 6 | 7 | # forces the Windows platform instead of the default one 8 | # configure options only for cross compile 9 | ext.cross_platform = %w( i386-mswin32 x86-mingw32 ) 10 | end 11 | 12 | CLEAN.include %w(**/*.{o,bundle,jar,so,obj,pdb,lib,def,exp,log} ext/*/Makefile ext/*/conftest.dSYM lib/1.{8,9}}) 13 | 14 | desc "Compile the Ragel state machines" 15 | task :ragel do 16 | Dir.chdir 'ext/thin_parser' do 17 | target = "parser.c" 18 | File.unlink target if File.exist? target 19 | sh "ragel parser.rl -G2 -o #{target}" 20 | raise "Failed to compile Ragel state machine" unless File.exist? target 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /tasks/rdoc.rake: -------------------------------------------------------------------------------- 1 | require 'rdoc/task' 2 | 3 | CLEAN.include %w(doc/rdoc) 4 | 5 | RDoc::Task.new do |rdoc| 6 | rdoc.rdoc_dir = 'doc/rdoc' 7 | rdoc.options += ['--quiet', '--title', Thin::NAME, 8 | "--opname", "index.html", 9 | "--line-numbers", 10 | "--main", "README.md", 11 | "--inline-source"] 12 | rdoc.template = "site/rdoc.rb" 13 | rdoc.main = "README.md" 14 | rdoc.title = Thin::NAME 15 | rdoc.rdoc_files.add %w(README.md) + 16 | FileList['lib/**/*.rb'] + 17 | FileList['bin/*'] 18 | end 19 | 20 | namespace :rdoc do 21 | desc 'Upload rdoc to code.macournoyer.com' 22 | task :upload => :rdoc do 23 | upload "doc/rdoc", 'thin/doc', :replace => true 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /tasks/site.rake: -------------------------------------------------------------------------------- 1 | namespace :site do 2 | task :build do 3 | mkdir_p 'tmp/site/images' 4 | cd 'tmp/site' do 5 | sh "SITE_ROOT='/thin' ruby ../../site/thin.rb --dump" 6 | end 7 | cp 'site/style.css', 'tmp/site' 8 | cp_r Dir['site/images/*'], 'tmp/site/images' 9 | end 10 | 11 | desc 'Upload website to code.macournoyer.com' 12 | task :upload => 'site:build' do 13 | upload 'tmp/site/*', 'thin' 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /tasks/spec.rake: -------------------------------------------------------------------------------- 1 | CLEAN.include %w(coverage tmp log) 2 | 3 | require 'rspec/core/rake_task' 4 | 5 | PERF_SPECS = FileList['spec/perf/*_spec.rb'] 6 | WIN_SPECS = %w( 7 | spec/backends/unix_server_spec.rb 8 | spec/controllers/service_spec.rb 9 | spec/daemonizing_spec.rb 10 | spec/server/unix_socket_spec.rb 11 | spec/server/swiftiply_spec.rb 12 | ) 13 | # HACK Event machine causes some problems when running multiple 14 | # tests in the same VM so we split the specs in groups before I find 15 | # a better solution... 16 | SPEC_GROUPS = [ 17 | %w(spec/server/threaded_spec.rb spec/server/tcp_spec.rb), 18 | %w(spec/daemonizing_spec.rb), 19 | %w(spec/server/stopping_spec.rb), 20 | ] 21 | SPECS = FileList['spec/**/*_spec.rb'] - PERF_SPECS - SPEC_GROUPS.flatten 22 | 23 | def spec_task(name, specs) 24 | RSpec::Core::RakeTask.new(name) do |t| 25 | t.rspec_opts = ["-c", "-f documentation"] 26 | t.pattern = specs 27 | end 28 | end 29 | 30 | desc "Run all main specs" 31 | spec_task "spec:main", SPECS 32 | task :spec => [:compile, "spec:main"] 33 | 34 | SPEC_GROUPS.each_with_index do |files, i| 35 | task_name = "spec:group:#{i}" 36 | desc "Run specs sub-group ##{i}" 37 | spec_task task_name, files 38 | task :spec => task_name 39 | end 40 | 41 | desc "Run all performance examples" 42 | spec_task 'spec:perf', PERF_SPECS 43 | 44 | task :check_benchmark_unit_gem do 45 | begin 46 | require 'benchmark_unit' 47 | rescue LoadError 48 | abort "To run specs, install benchmark_unit gem" 49 | end 50 | end 51 | 52 | task 'spec:perf' => :check_benchmark_unit_gem 53 | -------------------------------------------------------------------------------- /tasks/stats.rake: -------------------------------------------------------------------------------- 1 | desc 'Show some stats about the code' 2 | task :stats do 3 | line_count = proc do |path| 4 | Dir[path].collect { |f| File.open(f).readlines.reject { |l| l =~ /(^\s*(\#|\/\*))|^\s*$/ }.size }.inject(0){ |sum,n| sum += n } 5 | end 6 | comment_count = proc do |path| 7 | Dir[path].collect { |f| File.open(f).readlines.select { |l| l =~ /^\s*\#/ }.size }.inject(0) { |sum,n| sum += n } 8 | end 9 | lib = line_count['lib/**/*.rb'] 10 | comment = comment_count['lib/**/*.rb'] 11 | ext = line_count['ext/**/*.{c,h}'] 12 | spec = line_count['spec/**/*.rb'] 13 | 14 | comment_ratio = '%1.2f' % (comment.to_f / lib.to_f) 15 | spec_ratio = '%1.2f' % (spec.to_f / lib.to_f) 16 | 17 | puts '/======================\\' 18 | puts '| Part LOC |' 19 | puts '|======================|' 20 | puts "| lib #{lib.to_s.ljust(5)}|" 21 | puts "| lib comments #{comment.to_s.ljust(5)}|" 22 | puts "| ext #{ext.to_s.ljust(5)}|" 23 | puts "| spec #{spec.to_s.ljust(5)}|" 24 | puts '| ratios: |' 25 | puts "| lib/comment #{comment_ratio.to_s.ljust(5)}|" 26 | puts "| lib/spec #{spec_ratio.to_s.ljust(5)}|" 27 | puts '\======================/' 28 | end 29 | -------------------------------------------------------------------------------- /thin.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path("../lib", __FILE__) 2 | 3 | # Maintain your gem's version: 4 | require "thin/version" 5 | 6 | # Describe your gem and declare its dependencies: 7 | Thin::GemSpec ||= Gem::Specification.new do |s| 8 | s.name = Thin::NAME 9 | s.version = Thin::VERSION::STRING 10 | s.platform = Thin.win? ? Gem::Platform::CURRENT : Gem::Platform::RUBY 11 | s.summary = "A thin and fast web server" 12 | s.author = "Marc-Andre Cournoyer" 13 | s.email = 'macournoyer@gmail.com' 14 | s.homepage = 'https://github.com/macournoyer/thin' 15 | s.licenses = ["GPL-2.0+", "Ruby"] 16 | s.executables = %w( thin ) 17 | 18 | s.metadata = { 19 | 'source_code_uri' => 'https://github.com/macournoyer/thin', 20 | 'changelog_uri' => 'https://github.com/macournoyer/thin/blob/master/CHANGELOG' 21 | } 22 | 23 | s.required_ruby_version = '>= 2.6' 24 | 25 | s.add_dependency 'rack', '>= 1', '< 4' 26 | s.add_dependency 'eventmachine', '~> 1.0', '>= 1.0.4' 27 | s.add_dependency 'daemons', '~> 1.0', '>= 1.0.9' unless Thin.win? 28 | s.add_dependency 'logger' 29 | 30 | s.files = %w(CHANGELOG README.md Rakefile) + 31 | Dir["{bin,doc,example,lib}/**/*"] - Dir["lib/thin_parser.*"] + 32 | Dir["ext/**/*.{h,c,rb,rl}"] 33 | 34 | if Thin.win? 35 | s.files += Dir["lib/*/thin_parser.*"] 36 | else 37 | s.extensions = Dir["ext/**/extconf.rb"] 38 | end 39 | 40 | s.require_path = "lib" 41 | s.bindir = "bin" 42 | end 43 | --------------------------------------------------------------------------------