gross 1.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG +410 -0
  3. data/README.md +102 -0
  4. data/Rakefile +25 -0
  5. data/bin/thin +6 -0
  6. data/example/adapter.rb +32 -0
  7. data/example/async_app.ru +126 -0
  8. data/example/async_chat.ru +247 -0
  9. data/example/async_tailer.ru +100 -0
  10. data/example/config.ru +22 -0
  11. data/example/monit_sockets +20 -0
  12. data/example/monit_unixsock +20 -0
  13. data/example/myapp.rb +1 -0
  14. data/example/ramaze.ru +12 -0
  15. data/example/thin.god +80 -0
  16. data/example/thin_solaris_smf.erb +36 -0
  17. data/example/thin_solaris_smf.readme.txt +150 -0
  18. data/example/vlad.rake +72 -0
  19. data/ext/thin_parser/common.rl +59 -0
  20. data/ext/thin_parser/ext_help.h +14 -0
  21. data/ext/thin_parser/extconf.rb +6 -0
  22. data/ext/thin_parser/parser.c +1447 -0
  23. data/ext/thin_parser/parser.h +49 -0
  24. data/ext/thin_parser/parser.rl +152 -0
  25. data/ext/thin_parser/thin.c +435 -0
  26. data/lib/rack/adapter/loader.rb +75 -0
  27. data/lib/rack/adapter/rails.rb +178 -0
  28. data/lib/thin.rb +45 -0
  29. data/lib/thin/backends/base.rb +167 -0
  30. data/lib/thin/backends/swiftiply_client.rb +56 -0
  31. data/lib/thin/backends/tcp_server.rb +34 -0
  32. data/lib/thin/backends/unix_server.rb +56 -0
  33. data/lib/thin/command.rb +53 -0
  34. data/lib/thin/connection.rb +215 -0
  35. data/lib/thin/controllers/cluster.rb +178 -0
  36. data/lib/thin/controllers/controller.rb +189 -0
  37. data/lib/thin/controllers/service.rb +76 -0
  38. data/lib/thin/controllers/service.sh.erb +39 -0
  39. data/lib/thin/daemonizing.rb +180 -0
  40. data/lib/thin/headers.rb +40 -0
  41. data/lib/thin/logging.rb +174 -0
  42. data/lib/thin/request.rb +162 -0
  43. data/lib/thin/response.rb +117 -0
  44. data/lib/thin/runner.rb +238 -0
  45. data/lib/thin/server.rb +290 -0
  46. data/lib/thin/stats.html.erb +216 -0
  47. data/lib/thin/stats.rb +52 -0
  48. data/lib/thin/statuses.rb +44 -0
  49. data/lib/thin/version.rb +32 -0
  50. metadata +156 -0
@@ -0,0 +1,189 @@
1
+ require 'yaml'
2
+
3
+ module Thin
4
+ # Error raised that will abort the process and print not backtrace.
5
+ class RunnerError < RuntimeError; end
6
+
7
+ # Raised when a mandatory option is missing to run a command.
8
+ class OptionRequired < RunnerError
9
+ def initialize(option)
10
+ super("#{option} option required")
11
+ end
12
+ end
13
+
14
+ # Raised when an option is not valid.
15
+ class InvalidOption < RunnerError; end
16
+
17
+ # Build and control Thin servers.
18
+ # Hey Controller pattern is not only for web apps yo!
19
+ module Controllers
20
+ # Controls one Thin server.
21
+ # Allow to start, stop, restart and configure a single thin server.
22
+ class Controller
23
+ include Logging
24
+
25
+ # Command line options passed to the thin script
26
+ attr_accessor :options
27
+
28
+ def initialize(options)
29
+ @options = options
30
+
31
+ if @options[:socket]
32
+ @options.delete(:address)
33
+ @options.delete(:port)
34
+ end
35
+ end
36
+
37
+ def start
38
+ # Constantize backend class
39
+ @options[:backend] = eval(@options[:backend], TOPLEVEL_BINDING) if @options[:backend]
40
+
41
+ server = Server.new(@options[:socket] || @options[:address], # Server detects kind of socket
42
+ @options[:port], # Port ignored on UNIX socket
43
+ @options)
44
+
45
+ # Set options
46
+ server.pid_file = @options[:pid]
47
+ server.log_file = @options[:log]
48
+ server.timeout = @options[:timeout]
49
+ server.maximum_connections = @options[:max_conns]
50
+ server.maximum_persistent_connections = @options[:max_persistent_conns]
51
+ server.threaded = @options[:threaded]
52
+ server.no_epoll = @options[:no_epoll] if server.backend.respond_to?(:no_epoll=)
53
+ server.threadpool_size = @options[:threadpool_size] if server.threaded?
54
+
55
+ # ssl support
56
+ if @options[:ssl]
57
+ server.ssl = true
58
+ server.ssl_options = { :private_key_file => @options[:ssl_key_file], :cert_chain_file => @options[:ssl_cert_file], :verify_peer => !@options[:ssl_disable_verify], :ssl_version => @options[:ssl_version], :cipher_list => @options[:ssl_cipher_list]}
59
+ end
60
+
61
+ # Detach the process, after this line the current process returns
62
+ server.daemonize if @options[:daemonize]
63
+
64
+ # +config+ must be called before changing privileges since it might require superuser power.
65
+ server.config
66
+
67
+ server.change_privilege @options[:user], @options[:group] if @options[:user] && @options[:group]
68
+
69
+ # If a Rack config file is specified we eval it inside a Rack::Builder block to create
70
+ # a Rack adapter from it. Or else we guess which adapter to use and load it.
71
+ if @options[:rackup]
72
+ server.app = load_rackup_config
73
+ else
74
+ server.app = load_adapter
75
+ end
76
+
77
+ # If a prefix is required, wrap in Rack URL mapper
78
+ server.app = Rack::URLMap.new(@options[:prefix] => server.app) if @options[:prefix]
79
+
80
+ # If a stats URL is specified, wrap in Stats adapter
81
+ server.app = Stats::Adapter.new(server.app, @options[:stats]) if @options[:stats]
82
+
83
+ # Register restart procedure which just start another process with same options,
84
+ # so that's why this is done here.
85
+ server.on_restart { Command.run(:start, @options) }
86
+
87
+ server.start
88
+ end
89
+
90
+ def stop
91
+ raise OptionRequired, :pid unless @options[:pid]
92
+
93
+ tail_log(@options[:log]) do
94
+ if Server.kill(@options[:pid], @options[:force] ? 0 : (@options[:timeout] || 60))
95
+ wait_for_file :deletion, @options[:pid]
96
+ end
97
+ end
98
+ end
99
+
100
+ def restart
101
+ raise OptionRequired, :pid unless @options[:pid]
102
+
103
+ tail_log(@options[:log]) do
104
+ if Server.restart(@options[:pid])
105
+ wait_for_file :creation, @options[:pid]
106
+ end
107
+ end
108
+ end
109
+
110
+ def config
111
+ config_file = @options.delete(:config) || raise(OptionRequired, :config)
112
+
113
+ # Stringify keys
114
+ @options.keys.each { |o| @options[o.to_s] = @options.delete(o) }
115
+
116
+ File.open(config_file, 'w') { |f| f << @options.to_yaml }
117
+ log_info "Wrote configuration to #{config_file}"
118
+ end
119
+
120
+ protected
121
+ # Wait for a pid file to either be created or deleted.
122
+ def wait_for_file(state, file)
123
+ Timeout.timeout(@options[:timeout] || 30) do
124
+ case state
125
+ when :creation then sleep 0.1 until File.exist?(file)
126
+ when :deletion then sleep 0.1 while File.exist?(file)
127
+ end
128
+ end
129
+ end
130
+
131
+ # Tail the log file of server +number+ during the execution of the block.
132
+ def tail_log(log_file)
133
+ if log_file
134
+ tail_thread = tail(log_file)
135
+ yield
136
+ tail_thread.kill
137
+ else
138
+ yield
139
+ end
140
+ end
141
+
142
+ # Acts like GNU tail command. Taken from Rails.
143
+ def tail(file)
144
+ cursor = File.exist?(file) ? File.size(file) : 0
145
+ last_checked = Time.now
146
+ tail_thread = Thread.new do
147
+ Thread.pass until File.exist?(file)
148
+ File.open(file, 'r') do |f|
149
+ loop do
150
+ f.seek cursor
151
+ if f.mtime > last_checked
152
+ last_checked = f.mtime
153
+ contents = f.read
154
+ cursor += contents.length
155
+ print contents
156
+ STDOUT.flush
157
+ end
158
+ sleep 0.1
159
+ end
160
+ end
161
+ end
162
+ sleep 1 if File.exist?(file) # HACK Give the thread a little time to open the file
163
+ tail_thread
164
+ end
165
+
166
+ private
167
+ def load_adapter
168
+ adapter = @options[:adapter] || Rack::Adapter.guess(@options[:chdir])
169
+ log_info "Using #{adapter} adapter"
170
+ Rack::Adapter.for(adapter, @options)
171
+ rescue Rack::AdapterNotFound => e
172
+ raise InvalidOption, e.message
173
+ end
174
+
175
+ def load_rackup_config
176
+ ENV['RACK_ENV'] = @options[:environment]
177
+ case @options[:rackup]
178
+ when /\.rb$/
179
+ Kernel.load(@options[:rackup])
180
+ Object.const_get(File.basename(@options[:rackup], '.rb').capitalize.to_sym)
181
+ when /\.ru$/
182
+ Rack::Adapter.load(@options[:rackup])
183
+ else
184
+ raise "Invalid rackup file. please specify either a .ru or .rb file"
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,76 @@
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
@@ -0,0 +1,39 @@
1
+ #!/bin/sh
2
+ ### BEGIN INIT INFO
3
+ # Provides: thin
4
+ # Required-Start: $local_fs $remote_fs
5
+ # Required-Stop: $local_fs $remote_fs
6
+ # Default-Start: 2 3 4 5
7
+ # Default-Stop: S 0 1 6
8
+ # Short-Description: thin initscript
9
+ # Description: thin
10
+ ### END INIT INFO
11
+
12
+ # Original author: Forrest Robertson
13
+
14
+ # Do NOT "set -e"
15
+
16
+ DAEMON=<%= Command.script %>
17
+ SCRIPT_NAME=<%= INITD_PATH %>
18
+ CONFIG_PATH=<%= config_files_path %>
19
+
20
+ # Exit if the package is not installed
21
+ [ -x "$DAEMON" ] || exit 0
22
+
23
+ case "$1" in
24
+ start)
25
+ $DAEMON start --all $CONFIG_PATH
26
+ ;;
27
+ stop)
28
+ $DAEMON stop --all $CONFIG_PATH
29
+ ;;
30
+ restart)
31
+ $DAEMON restart --all $CONFIG_PATH
32
+ ;;
33
+ *)
34
+ echo "Usage: $SCRIPT_NAME {start|stop|restart}" >&2
35
+ exit 3
36
+ ;;
37
+ esac
38
+
39
+ :
@@ -0,0 +1,180 @@
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
+ # Turns the current script into a daemon process that detaches from the console.
39
+ def daemonize
40
+ raise PlatformNotSupported, 'Daemonizing is not supported on Windows' if Thin.win?
41
+ raise ArgumentError, 'You must specify a pid_file to daemonize' unless @pid_file
42
+
43
+ remove_stale_pid_file
44
+
45
+ pwd = Dir.pwd # Current directory is changed during daemonization, so store it
46
+
47
+ # HACK we need to create the directory before daemonization to prevent a bug under 1.9
48
+ # ignoring all signals when the directory is created after daemonization.
49
+ FileUtils.mkdir_p File.dirname(@pid_file)
50
+ FileUtils.mkdir_p File.dirname(@log_file)
51
+
52
+ Daemonize.daemonize(File.expand_path(@log_file), name)
53
+
54
+ Dir.chdir(pwd)
55
+
56
+ write_pid_file
57
+
58
+ at_exit do
59
+ log_info "Exiting!"
60
+ remove_pid_file
61
+ end
62
+ end
63
+
64
+ # Change privileges of the process
65
+ # to the specified user and group.
66
+ def change_privilege(user, group=user)
67
+ log_info "Changing process privilege to #{user}:#{group}"
68
+
69
+ uid, gid = Process.euid, Process.egid
70
+ target_uid = Etc.getpwnam(user).uid
71
+ target_gid = Etc.getgrnam(group).gid
72
+
73
+ if uid != target_uid || gid != target_gid
74
+ # Change PID file ownership
75
+ File.chown(target_uid, target_gid, @pid_file) if File.exists?(@pid_file)
76
+
77
+ # Change process ownership
78
+ Process.initgroups(user, target_gid)
79
+ Process::GID.change_privilege(target_gid)
80
+ Process::UID.change_privilege(target_uid)
81
+ end
82
+ rescue Errno::EPERM => e
83
+ log_info "Couldn't change user and group to #{user}:#{group}: #{e}"
84
+ end
85
+
86
+ # Register a proc to be called to restart the server.
87
+ def on_restart(&block)
88
+ @on_restart = block
89
+ end
90
+
91
+ # Restart the server.
92
+ def restart
93
+ if @on_restart
94
+ log_info 'Restarting ...'
95
+ stop
96
+ remove_pid_file
97
+ @on_restart.call
98
+ EM.next_tick { exit! }
99
+ end
100
+ end
101
+
102
+ module ClassMethods
103
+ # Send a QUIT or INT (if timeout is +0+) signal the process which
104
+ # PID is stored in +pid_file+.
105
+ # If the process is still running after +timeout+, KILL signal is
106
+ # sent.
107
+ def kill(pid_file, timeout=60)
108
+ if timeout == 0
109
+ send_signal('INT', pid_file, timeout)
110
+ else
111
+ send_signal('QUIT', pid_file, timeout)
112
+ end
113
+ end
114
+
115
+ # Restart the server by sending HUP signal.
116
+ def restart(pid_file)
117
+ send_signal('HUP', pid_file)
118
+ end
119
+
120
+ # Send a +signal+ to the process which PID is stored in +pid_file+.
121
+ def send_signal(signal, pid_file, timeout=60)
122
+ if pid = read_pid_file(pid_file)
123
+ Logging.log_info "Sending #{signal} signal to process #{pid} ... "
124
+ Process.kill(signal, pid)
125
+ Timeout.timeout(timeout) do
126
+ sleep 0.1 while Process.running?(pid)
127
+ end
128
+ else
129
+ raise PidFileNotFound, "Can't stop process, no PID found in #{pid_file}"
130
+ end
131
+ rescue Timeout::Error
132
+ Logging.log_info "Timeout!"
133
+ force_kill(pid, pid_file)
134
+ rescue Interrupt
135
+ force_kill(pid, pid_file)
136
+ rescue Errno::ESRCH # No such process
137
+ Logging.log_info "process not found!"
138
+ force_kill(pid, pid_file)
139
+ end
140
+
141
+ def force_kill(pid, pid_file)
142
+ Logging.log_info "Sending KILL signal to process #{pid} ... "
143
+ Process.kill("KILL", pid)
144
+ File.delete(pid_file) if File.exist?(pid_file)
145
+ end
146
+
147
+ def read_pid_file(file)
148
+ if File.file?(file) && pid = File.read(file)
149
+ pid.to_i
150
+ else
151
+ nil
152
+ end
153
+ end
154
+ end
155
+
156
+ protected
157
+ def remove_pid_file
158
+ File.delete(@pid_file) if @pid_file && File.exists?(@pid_file)
159
+ end
160
+
161
+ def write_pid_file
162
+ log_info "Writing PID to #{@pid_file}"
163
+ open(@pid_file,"w") { |f| f.write(Process.pid) }
164
+ File.chmod(0644, @pid_file)
165
+ end
166
+
167
+ # If PID file is stale, remove it.
168
+ def remove_stale_pid_file
169
+ if File.exist?(@pid_file)
170
+ if pid && Process.running?(pid)
171
+ raise PidFileExist, "#{@pid_file} already exists, seems like it's already running (process ID: #{pid}). " +
172
+ "Stop the process or delete #{@pid_file}."
173
+ else
174
+ log_info "Deleting stale PID file #{@pid_file}"
175
+ remove_pid_file
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end