Skip to content
Browse files

Refactoring thin script in several classes:

* Runner: main CLI runner, parse options and send the command to the controller.
* Controller: Configure and control a server or cluster (Cluster class now inherits from Controller).
  • Loading branch information...
1 parent 2a7e102 commit 401ade74083354c0a180324a00d18c50e7c5f88c @macournoyer macournoyer committed Feb 1, 2008
Showing with 213 additions and 185 deletions.
  1. +2 −164 bin/thin
  2. +2 −0 lib/thin.rb
  3. +2 −12 lib/thin/cluster.rb
  4. +7 −5 lib/thin/command.rb
  5. +75 −0 lib/thin/controller.rb
  6. +124 −0 lib/thin/runner.rb
  7. +0 −3 spec/cluster_spec.rb
  8. +0 −1 spec/command_spec.rb
  9. +1 −0 spec/spec_helper.rb
View
166 bin/thin
@@ -1,168 +1,6 @@
#!/usr/bin/env ruby
-# <tt>thin start</tt>: Starts the Rails app in the current directory.
+# Thin command line interface script.
# Run <tt>thin -h</tt> to get more usage.
require File.dirname(__FILE__) + '/../lib/thin'
-require 'optparse'
-require 'yaml'
-COMMANDS = %w(start stop restart config)
-
-# Default options values
-options = {
- :chdir => Dir.pwd,
- :environment => 'development',
- :address => '0.0.0.0',
- :port => 3000,
- :timeout => 60,
- :log => 'log/thin.log',
- :pid => 'tmp/pids/thin.pid',
- :servers => 1 # no cluster
-}
-
-# NOTE: If you add an option here make sure the key in the +options+ hash is the
-# same as the name of the command line option.
-# +option+ keys are use to build the command line to launch a cluster,
-# see <tt>lib/thin/cluster.rb</tt>.
-opts = OptionParser.new do |opts|
- opts.banner = "Usage: thin [options] #{COMMANDS.join('|')}"
-
- opts.separator ""
- opts.separator "Server options:"
-
- opts.on("-a", "--address HOST", "bind to HOST address (default: 0.0.0.0)") { |host| options[:address] = host }
- opts.on("-p", "--port PORT", "use PORT (default: 3000)") { |port| options[:port] = port.to_i }
- opts.on("-S", "--socket PATH", "bind to unix domain socket") { |file| options[:socket] = file }
- opts.on("-e", "--environment ENV", "Rails environment (default: development)") { |env| options[:environment] = env }
- opts.on("-c", "--chdir PATH", "Change to dir before starting") { |dir| options[:chdir] = File.expand_path(dir) }
- opts.on("-t", "--timeout SEC", "Request or command timeout in sec",
- "(default: #{options[:timeout]})") { |sec| options[:timeout] = sec.to_i }
- opts.on( "--prefix PATH", "Mount the app under PATH (start with /)") { |path| options[:prefix] = path }
- opts.on( "--stats PATH", "Mount the Stats adapter under PATH") { |path| options[:stats] = path }
-
- opts.separator ""
- opts.separator "Daemon options:"
-
- opts.on("-d", "--daemonize", "Run daemonized in the background") { options[:daemonize] = true }
- opts.on("-l", "--log FILE", "File to redirect output",
- "(default: #{options[:log]})") { |file| options[:log] = file }
- opts.on("-P", "--pid FILE", "File to store PID",
- "(default: #{options[:pid]})") { |file| options[:pid] = file }
- opts.on("-u", "--user NAME", "User to run daemon as (use with -g)") { |user| options[:user] = user }
- opts.on("-g", "--group NAME", "Group to run daemon as (use with -u)") { |group| options[:group] = group }
-
- opts.separator ""
- opts.separator "Cluster options:"
-
- opts.on("-s", "--servers NUM", "Number of servers to start",
- "set a value >1 to start a cluster") { |num| options[:servers] = num.to_i }
- opts.on("-o", "--only NUM", "Send command to only one server of the cluster") { |only| options[:only] = only }
- opts.on("-C", "--config PATH", "Load options from a config file") { |file| options[:config] = file }
-
- opts.separator ""
- opts.separator "Common options:"
-
- opts.on_tail("-D", "--debug", "Set debbuging on") { $DEBUG = true }
- opts.on_tail("-V", "--trace", "Set tracing on") { $TRACE = true }
- opts.on_tail("-h", "--help", "Show this message") { puts opts; exit }
- opts.on_tail('-v', '--version', "Show version") { puts Thin::SERVER; exit }
-end
-
-
-# == Utilities
-
-def cluster?(options)
- options[:only] || (options[:servers] && options[:servers] > 1)
-end
-
-def load_options_from_config_file!(options)
- if file = options.delete(:config)
- YAML.load_file(file).each { |key, value| options[key.to_sym] = value }
- end
-end
-
-
-# == Commands definitions
-
-def start(options)
- load_options_from_config_file! options
-
- if cluster?(options)
- Thin::Cluster.new(options).start
- else
- if options[:socket]
- server = Thin::Server.new(options[:socket])
- else
- server = Thin::Server.new(options[:address], options[:port])
- end
-
- server.pid_file = options[:pid]
- server.log_file = options[:log]
- server.timeout = options[:timeout]
-
- if options[:daemonize]
- server.daemonize
- server.change_privilege options[:user], options[:group] if options[:user] && options[:group]
- end
-
- server.app = Rack::Adapter::Rails.new(options.merge(:root => options[:chdir]))
-
- # If a prefix is required, wrap in Rack URL mapper
- server.app = Rack::URLMap.new(options[:prefix] => server.app) if options[:prefix]
-
- # If a stats are required, wrap in Stats adapter
- server.app = Thin::Stats::Adapter.new(server.app, options[:stats]) if options[:stats]
-
- # Register restart procedure
- server.on_restart { Thin::Command.run(:start, options) }
-
- server.start
- end
-end
-
-def stop(options)
- load_options_from_config_file! options
-
- if cluster?(options)
- Thin::Cluster.new(options).stop
- else
- Thin::Server.kill(options[:pid], options[:timeout])
- end
-end
-
-def restart(options)
- load_options_from_config_file! options
-
- if cluster?(options)
- Thin::Cluster.new(options).restart
- else
- Thin::Server.restart(options[:pid])
- end
-end
-
-def config(options)
- config_file = options.delete(:config) || abort('config option required')
-
- # Stringify keys
- options.keys.each { |o| options[o.to_s] = options.delete(o) }
-
- File.open(config_file, 'w') { |f| f << options.to_yaml }
- puts "Wrote configuration to #{config_file}"
-end
-
-
-# == Runs the command
-
-opts.parse! ARGV
-command = ARGV[0]
-
-Dir.chdir(options[:chdir])
-
-if COMMANDS.include?(command)
- send(command, options)
-elsif command.nil?
- puts "Command required"
- puts opts
- exit 1
-else
- abort "Invalid command : #{command}"
-end
+Thin::Runner.new(ARGV).run!
View
2 lib/thin.rb
@@ -14,11 +14,13 @@ module Thin
autoload :Cluster, 'thin/cluster'
autoload :Command, 'thin/command'
autoload :Connection, 'thin/connection'
+ autoload :Controller, 'thin/controller'
autoload :Daemonizable, 'thin/daemonizing'
autoload :Logging, 'thin/logging'
autoload :Headers, 'thin/headers'
autoload :Request, 'thin/request'
autoload :Response, 'thin/response'
+ autoload :Runner, 'thin/runner'
autoload :Server, 'thin/server'
autoload :Stats, 'thin/stats'
end
View
14 lib/thin/cluster.rb
@@ -3,25 +3,15 @@ module Thin
# * Generate start and stop commands and run them.
# * Inject the port or socket number in the pid and log filenames.
# Servers are started throught the +thin+ command-line script.
- class Cluster
- include Logging
-
- # Path to the +thin+ script used to control the servers.
- # Leave this to default to use the one in the path.
- attr_accessor :script
-
+ class Cluster < Controller
# Number of servers in the cluster.
attr_accessor :size
-
- # Command line options passed to the thin script
- attr_accessor :options
-
+
# Create a new cluster of servers launched using +options+.
def initialize(options)
@options = options.merge(:daemonize => true)
@size = @options.delete(:servers)
@only = @options.delete(:only)
- @script = $PROGRAM_NAME
if socket
@options.delete(:address)
View
12 lib/thin/command.rb
@@ -3,14 +3,16 @@ module Thin
class Command
include Logging
- # Path to the +thin+ script used to control the servers.
- # Leave this to default to use the one in the path.
- attr_accessor :script
+ class << self
+ # Path to the +thin+ script used to control the servers.
+ # Leave this to default to use the one in the path.
+ attr_accessor :script
+ @script = $PROGRAM_NAME
+ end
def initialize(name, options={})
@name = name
@options = options
- @script = $PROGRAM_NAME
end
def self.run(*args)
@@ -34,7 +36,7 @@ def shellify
else "--#{name.to_s.tr('_', '-')}=#{value.inspect}"
end
end
- "#{@script} #{@name} #{shellified_options.compact.join(' ')}"
+ "#{self.class.script} #{@name} #{shellified_options.compact.join(' ')}"
end
end
end
View
75 lib/thin/controller.rb
@@ -0,0 +1,75 @@
+require 'yaml'
+
+module Thin
+ # Raised when a mandatory option is missing to run a command.
+ class OptionRequired < RuntimeError
+ def initialize(option)
+ super("#{option} option required")
+ end
+ end
+
+ # Control a Thin server.
+ # Allow to start, stop, restart and configure a single thin server.
+ class Controller
+ include Logging
+
+ # Command line options passed to the thin script
+ attr_accessor :options
+
+ def initialize(options)
+ @options = options
+ end
+
+ def start
+ if @options[:socket]
+ server = Server.new(@options[:socket])
+ else
+ server = Server.new(@options[:address], @options[:port])
+ end
+
+ server.pid_file = @options[:pid]
+ server.log_file = @options[:log]
+ server.timeout = @options[:timeout]
+
+ if @options[:daemonize]
+ server.daemonize
+ server.change_privilege @options[:user], @options[:group] if @options[:user] && @options[:group]
+ end
+
+ server.app = Rack::Adapter::Rails.new(@options.merge(:root => @options[:chdir]))
+
+ # If a prefix is required, wrap in Rack URL mapper
+ server.app = Rack::URLMap.new(@options[:prefix] => server.app) if @options[:prefix]
+
+ # If a stats are required, wrap in Stats adapter
+ server.app = Stats::Adapter.new(server.app, @options[:stats]) if @options[:stats]
+
+ # Register restart procedure
+ server.on_restart { Command.run(:start, @options) }
+
+ server.start
+ end
+
+ def stop
+ raise OptionRequired, :pid unless @options[:pid]
+
+ Server.kill(@options[:pid], @options[:timeout] || 60)
+ end
+
+ def restart
+ raise OptionRequired, :pid unless @options[:pid]
+
+ Server.restart(@options[:pid])
+ end
+
+ def config
+ config_file = @options.delete(:config) || raise(OptionRequired, :config)
+
+ # Stringify keys
+ @options.keys.each { |o| @options[o.to_s] = @options.delete(o) }
+
+ File.open(config_file, 'w') { |f| f << @options.to_yaml }
+ log ">> Wrote configuration to #{config_file}"
+ end
+ end
+end
View
124 lib/thin/runner.rb
@@ -0,0 +1,124 @@
+require 'optparse'
+require 'yaml'
+
+module Thin
+ # CLI runner.
+ # Parse options and send command to the correct Controller.
+ class Runner
+ COMMANDS = %w(start stop restart config)
+
+ def initialize(argv)
+ @argv = argv
+
+ # Default options values
+ @options = {
+ :chdir => Dir.pwd,
+ :environment => 'development',
+ :address => '0.0.0.0',
+ :port => 3000,
+ :timeout => 60,
+ :log => 'log/thin.log',
+ :pid => 'tmp/pids/thin.pid',
+ :servers => 1 # no cluster
+ }
+
+ @parser = parser
+ end
+
+ def parser
+ # NOTE: If you add an option here make sure the key in the +options+ hash is the
+ # same as the name of the command line option.
+ # +option+ keys are used to build the command line to launch other processes,
+ # see <tt>lib/thin/command.rb</tt>.
+ OptionParser.new do |opts|
+ opts.banner = "Usage: thin [options] #{COMMANDS.join('|')}"
+
+ opts.separator ""
+ opts.separator "Server options:"
+
+ opts.on("-a", "--address HOST", "bind to HOST address " +
+ "(default: #{@options[:address]})") { |host| @options[:address] = host }
+ opts.on("-p", "--port PORT", "use PORT (default: #{@options[:port]})") { |port| @options[:port] = port.to_i }
+ opts.on("-S", "--socket PATH", "bind to unix domain socket") { |file| @options[:socket] = file }
+ opts.on("-e", "--environment ENV", "Rails environment " +
+ "(default: #{@options[:environment]})") { |env| @options[:environment] = env }
+ opts.on("-c", "--chdir PATH", "Change to dir before starting") { |dir| @options[:chdir] = File.expand_path(dir) }
+ opts.on("-t", "--timeout SEC", "Request or command timeout in sec " +
+ "(default: #{@options[:timeout]})") { |sec| @options[:timeout] = sec.to_i }
+ opts.on( "--prefix PATH", "Mount the app under PATH (start with /)") { |path| @options[:prefix] = path }
+ opts.on( "--stats PATH", "Mount the Stats adapter under PATH") { |path| @options[:stats] = path }
+
+ opts.separator ""
+ opts.separator "Daemon options:"
+
+ opts.on("-d", "--daemonize", "Run daemonized in the background") { @options[:daemonize] = true }
+ opts.on("-l", "--log FILE", "File to redirect output " +
+ "(default: #{@options[:log]})") { |file| @options[:log] = file }
+ opts.on("-P", "--pid FILE", "File to store PID " +
+ "(default: #{@options[:pid]})") { |file| @options[:pid] = file }
+ opts.on("-u", "--user NAME", "User to run daemon as (use with -g)") { |user| @options[:user] = user }
+ opts.on("-g", "--group NAME", "Group to run daemon as (use with -u)") { |group| @options[:group] = group }
+
+ opts.separator ""
+ opts.separator "Cluster options:"
+
+ opts.on("-s", "--servers NUM", "Number of servers to start",
+ "set a value >1 to start a cluster") { |num| @options[:servers] = num.to_i }
+ opts.on("-o", "--only NUM", "Send command to only one server of the cluster") { |only| @options[:only] = only }
+ opts.on("-C", "--config PATH", "Load options from a config file") { |file| @options[:config] = file }
+
+ opts.separator ""
+ opts.separator "Common options:"
+
+ opts.on_tail("-D", "--debug", "Set debbuging on") { $DEBUG = true }
+ opts.on_tail("-V", "--trace", "Set tracing on") { $TRACE = true }
+ opts.on_tail("-h", "--help", "Show this message") { puts opts; exit }
+ opts.on_tail('-v', '--version', "Show version") { puts Thin::SERVER; exit }
+ end
+ end
+
+ # Parse the current shell arguments and run the command.
+ # Exits on error.
+ def run!
+ @parser.parse! @argv
+ command = @argv[0]
+
+ Dir.chdir(@options[:chdir])
+
+ if COMMANDS.include?(command)
+ run_command(command)
+ elsif command.nil?
+ puts "Command required"
+ puts @parser
+ exit 1
+ else
+ abort "Invalid command : #{command}"
+ end
+ end
+
+ # Send the command to the controller: single instance or cluster.
+ def run_command(command)
+ load_options_from_config_file! unless command == 'config'
+
+ if cluster?
+ controller = Cluster.new(@options)
+ else
+ controller = Controller.new(@options)
+ end
+
+ controller.send(command)
+ end
+
+ # +true+ if we're controlling a cluster.
+ def cluster?
+ @options[:only] || (@options[:servers] && @options[:servers] > 1)
+ end
+
+ private
+ def load_options_from_config_file!
+ if file = @options.delete(:config)
+ YAML.load_file(file).each { |key, value| @options[key.to_sym] = value }
+ end
+ end
+ end
+end
View
3 spec/cluster_spec.rb
@@ -10,7 +10,6 @@
:log => 'thin.log',
:pid => 'thin.pid'
)
- @cluster.script = File.dirname(__FILE__) + '/../bin/thin'
@cluster.silent = true
end
@@ -60,7 +59,6 @@ def options_for_port(port)
:log => 'thin.log',
:pid => 'thin.pid'
)
- @cluster.script = File.dirname(__FILE__) + '/../bin/thin'
@cluster.silent = true
end
@@ -111,7 +109,6 @@ def options_for_socket(number)
:pid => 'thin.pid',
:only => 3001
)
- @cluster.script = File.dirname(__FILE__) + '/../bin/thin'
@cluster.silent = true
end
View
1 spec/command_spec.rb
@@ -3,7 +3,6 @@
describe Command do
before do
@command = Command.new(:start, :port => 3000, :daemonize => true, :log => 'hi.log', :pid => nil)
- @command.script = File.dirname(__FILE__) + '/../bin/thin'
@command.silent = true
end
View
1 spec/spec_helper.rb
@@ -11,6 +11,7 @@
include Thin
FileUtils.mkdir_p File.dirname(__FILE__) + '/../log'
+Command.script = File.dirname(__FILE__) + '/../bin/thin'
class TestRequest < Thin::Request
def initialize(path, verb='GET', params={})

0 comments on commit 401ade7

Please sign in to comment.
Something went wrong with that request. Please try again.