/
controller.rb
182 lines (152 loc) · 6.19 KB
/
controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
require 'yaml'
module Thin
# Error raised that will abort the process and print not backtrace.
class RunnerError < RuntimeError; end
# Raised when a mandatory option is missing to run a command.
class OptionRequired < RunnerError
def initialize(option)
super("#{option} option required")
end
end
# Raised when an option is not valid.
class InvalidOption < RunnerError; end
# Build and control Thin servers.
# Hey Controller pattern is not only for web apps yo!
module Controllers
# Controls one 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
if @options[:socket]
@options.delete(:address)
@options.delete(:port)
end
end
def start
# Constantize backend class
@options[:backend] = eval(@options[:backend], TOPLEVEL_BINDING) if @options[:backend]
server = Server.new(@options[:socket] || @options[:address], # Server detects kind of socket
@options[:port], # Port ignored on UNIX socket
@options)
# Set options
server.pid_file = @options[:pid]
server.log_file = @options[:log]
server.timeout = @options[:timeout]
server.maximum_connections = @options[:max_conns]
server.maximum_persistent_connections = @options[:max_persistent_conns]
server.threaded = @options[:threaded]
server.no_epoll = @options[:no_epoll] if server.backend.respond_to?(:no_epoll=)
# Detach the process, after this line the current process returns
server.daemonize if @options[:daemonize]
# +config+ must be called before changing privileges since it might require superuser power.
server.config
server.change_privilege @options[:user], @options[:group] if @options[:user] && @options[:group]
# If a Rack config file is specified we eval it inside a Rack::Builder block to create
# a Rack adapter from it. Or else we guess which adapter to use and load it.
if @options[:rackup]
server.app = load_rackup_config
else
server.app = load_adapter
end
# 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 URL is specified, wrap in Stats adapter
server.app = Stats::Adapter.new(server.app, @options[:stats]) if @options[:stats]
# Register restart procedure which just start another process with same options,
# so that's why this is done here.
server.on_restart { Command.run(:start, @options) }
server.start
end
def stop
raise OptionRequired, :pid unless @options[:pid]
tail_log(@options[:log]) do
if Server.kill(@options[:pid], @options[:force] ? 0 : (@options[:timeout] || 60))
wait_for_file :deletion, @options[:pid]
end
end
end
def restart
raise OptionRequired, :pid unless @options[:pid]
tail_log(@options[:log]) do
if Server.restart(@options[:pid])
wait_for_file :creation, @options[:pid]
end
end
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
protected
# Wait for a pid file to either be created or deleted.
def wait_for_file(state, file)
Timeout.timeout(@options[:timeout] || 30) do
case state
when :creation then sleep 0.1 until File.exist?(file)
when :deletion then sleep 0.1 while File.exist?(file)
end
end
end
# Tail the log file of server +number+ during the execution of the block.
def tail_log(log_file)
if log_file
tail_thread = tail(log_file)
yield
tail_thread.kill
else
yield
end
end
# Acts like GNU tail command. Taken from Rails.
def tail(file)
cursor = File.exist?(file) ? File.size(file) : 0
last_checked = Time.now
tail_thread = Thread.new do
Thread.pass until File.exist?(file)
File.open(file, 'r') do |f|
loop do
f.seek cursor
if f.mtime > last_checked
last_checked = f.mtime
contents = f.read
cursor += contents.length
print contents
STDOUT.flush
end
sleep 0.1
end
end
end
sleep 1 if File.exist?(file) # HACK Give the thread a little time to open the file
tail_thread
end
private
def load_adapter
adapter = @options[:adapter] || Rack::Adapter.guess(@options[:chdir])
log ">> Using #{adapter} adapter"
Rack::Adapter.for(adapter, @options)
rescue Rack::AdapterNotFound => e
raise InvalidOption, e.message
end
def load_rackup_config
case @options[:rackup]
when /\.rb$/
Kernel.load(@options[:rackup])
Object.const_get(File.basename(@options[:rackup], '.rb').capitalize.to_sym)
when /\.ru$/
rackup_code = File.read(@options[:rackup])
eval("Rack::Builder.new {( #{rackup_code}\n )}.to_app", TOPLEVEL_BINDING, @options[:rackup])
else
raise "Invalid rackup file. please specify either a .ru or .rb file"
end
end
end
end
end