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
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
| | # -*- encoding: binary -*-
# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
#
# Implements a DSL for configuring a yahns server.
# See http://yahns.yhbt.net/examples/yahns_multi.conf.rb for a full
# example configuration file.
class Yahns::Config # :nodoc:
APP_CLASS = {} # public, see yahns/rack for usage example
CfgBlock = Struct.new(:type, :ctx) # :nodoc:
attr_reader :config_file, :config_listeners, :set
attr_reader :qeggs, :app_ctx
def initialize(config_file = nil)
@config_file = config_file
@block = nil
config_reload!
end
def _check_in_block(ctx, var)
if ctx == nil
return var if @block == nil
msg = "#{var} must be called outside of #{@block.type}"
else
return var if @block && ctx == @block.type
msg = @block ? "may not be used inside a #{@block.type} block" :
"must be used with a #{ctx} block"
end
raise ArgumentError, msg
end
def postfork_cleanup
@app_ctx = @set = @qeggs = @app_instances = @config_file = nil
end
def config_reload! #:nodoc:
# app_instance:app_ctx is a 1:N relationship
@config_listeners = {} # name/address -> options
@app_ctx = []
@set = Hash.new(:unset)
@qeggs = {}
@app_instances = {}
# set defaults:
client_expire_threshold(0.5) # default is half of the open file limit
instance_eval(File.read(@config_file), @config_file) if @config_file
# working_directory binds immediately (easier error checking that way),
# now ensure any paths we changed are correctly set.
[ :pid, :stderr_path, :stdout_path ].each do |var|
String === (path = @set[var]) or next
path = File.expand_path(path)
File.writable?(path) || File.writable?(File.dirname(path)) or \
raise ArgumentError, "directory for #{var}=#{path} not writable"
end
end
def logger(obj)
var = :logger
%w(debug info warn error fatal).each do |m|
obj.respond_to?(m) and next
raise ArgumentError, "#{var}=#{obj} does not respond to method=#{m}"
end
if @block
if @block.ctx.respond_to?(:logger=)
@block.ctx.logger = obj
else
raise ArgumentError, "#{var} not valid inside #{@block.type}"
end
else
@set[var] = obj
end
end
def worker_processes(nr)
# TODO: allow zero
var = _check_in_block(nil, :worker_processes)
@set[var] = _check_int(var, nr, 1)
end
# sets the +path+ for the PID file of the yahns master process
def pid(path)
_set_path(:pid, path)
end
def stderr_path(path)
_set_path(:stderr_path, path)
end
def stdout_path(path)
_set_path(:stdout_path, path)
end
def value(var)
val = @set[var]
val == :unset ? nil : val
end
# sets the working directory for yahns. This ensures SIGUSR2 will
# start a new instance of yahns in this directory. This may be
# a symlink, a common scenario for Capistrano users. Unlike
# all other yahns configuration directives, this binds immediately
# for error checking and cannot be undone by unsetting it in the
# configuration file and reloading.
def working_directory(path)
var = :working_directory
@app_ctx.empty? or
raise ArgumentError, "#{var} must be declared before any apps"
# just let chdir raise errors
path = File.expand_path(path)
if @config_file &&
@config_file[0] != ?/ &&
! File.readable?("#{path}/#@config_file")
raise ArgumentError,
"config_file=#@config_file would not be accessible in" \
" #{var}=#{path}"
end
Dir.chdir(path)
@set[var] = ENV["PWD"] = path
end
# Runs worker processes as the specified +user+ and +group+.
# The master process always stays running as the user who started it.
# This switch will occur after calling the after_fork hooks, and only
# if the Worker#user method is not called in the after_fork hooks
# +group+ is optional and will not change if unspecified.
def user(user, group = nil)
var = :user
@block and raise "#{var} is not valid inside #{@block.type}"
# raises ArgumentError on invalid user/group
Etc.getpwnam(user)
Etc.getgrnam(group) if group
@set[var] = [ user, group ]
end
def _set_path(var, path) #:nodoc:
_check_in_block(nil, var)
case path
when NilClass, String
@set[var] = path
else
raise ArgumentError
end
end
def listen(address, options = {})
options = options.dup
var = _check_in_block(:app, :listen)
address = expand_addr(address)
String === address or
raise ArgumentError, "address=#{address.inspect} must be a string"
[ :umask, :backlog, :sndbuf, :rcvbuf ].each do |key|
value = options[key] or next
Integer === value or
raise ArgumentError, "#{var}: not an integer: #{key}=#{value.inspect}"
end
[ :ipv6only ].each do |key|
(value = options[key]).nil? and next
[ true, false ].include?(value) or
raise ArgumentError, "#{var}: not boolean: #{key}=#{value.inspect}"
end
options[:yahns_app_ctx] = @block.ctx
@config_listeners.include?(address) and
raise ArgumentError, "listen #{address} already in use"
@config_listeners[address] = options
end
# expands "unix:path/to/foo" to a socket relative to the current path
# expands pathnames of sockets if relative to "~" or "~username"
# expands "*:port and ":port" to "0.0.0.0:port"
def expand_addr(address) #:nodoc:
return "0.0.0.0:#{address}" if Integer === address
return address unless String === address
case address
when %r{\Aunix:(.*)\z}
File.expand_path($1)
when %r{\A~}
File.expand_path(address)
when %r{\A(?:\*:)?(\d+)\z}
"0.0.0.0:#$1"
when %r{\A\[([a-fA-F0-9:]+)\]\z}, %r/\A((?:\d+\.){3}\d+)\z/
canonicalize_tcp($1, 80)
when %r{\A\[([a-fA-F0-9:]+)\]:(\d+)\z}, %r{\A(.*):(\d+)\z}
canonicalize_tcp($1, $2.to_i)
else
address
end
end
def canonicalize_tcp(addr, port)
packed = Socket.pack_sockaddr_in(port, addr)
port, addr = Socket.unpack_sockaddr_in(packed)
/:/ =~ addr ? "[#{addr}]:#{port}" : "#{addr}:#{port}"
end
def queue(name = :default, &block)
var = :queue
qegg = @qeggs[name] ||= Yahns::QueueEgg.new
prev_block = @block
_check_in_block(:app, var) if prev_block
if block_given?
@block = CfgBlock.new(:queue, qegg)
instance_eval(&block)
@block = prev_block
end
prev_block.ctx.qegg = qegg if prev_block
end
# queue parameters (Yahns::QueueEgg)
%w(max_events worker_threads).each do |_v|
eval(
%Q(def #{_v}(val);) <<
%Q( _check_in_block(:queue, :#{_v});) <<
%Q( @block.ctx.__send__("#{_v}=", _check_int(:#{_v}, val, 1));) <<
%Q(end)
)
end
def _check_int(var, n, min)
Integer === n or raise ArgumentError, "not an integer: #{var}=#{n.inspect}"
n >= min or raise ArgumentError, "too low (< #{min}): #{var}=#{n.inspect}"
n
end
# global
def client_expire_threshold(val)
var = _check_in_block(nil, :client_expire_threshold)
case val
when Float
val <= 1.0 or raise ArgumentError, "#{var} must be <= 1.0 if a ratio"
when Integer
else
raise ArgumentError, "#{var} must be a float or integer"
end
@set[var] = val
end
# type = :rack
def app(type, *args, &block)
var = _check_in_block(nil, :app)
file = "yahns/#{type.to_s}"
begin
require file
rescue LoadError => e
raise ArgumentError, "#{type.inspect} is not a supported app type",
e.backtrace
end
klass = APP_CLASS[type] or
raise TypeError,
"#{var}: #{file} did not register #{type} in #{self.class}::APP_CLASS"
# apps may have multiple configurator contexts
app = @app_instances[klass.instance_key(*args)] = klass.new(*args)
ctx = app.config_context
if block_given?
@block = CfgBlock.new(:app, ctx)
instance_eval(&block)
@block = nil
end
@app_ctx << ctx
end
def _check_bool(var, val)
return val if [ true, false ].include?(val)
raise ArgumentError, "#{var} must be boolean"
end
# boolean config directives for app
%w(check_client_connection
output_buffering
persistent_connections).each do |_v|
eval(
%Q(def #{_v}(bool);) <<
%Q( _check_in_block(:app, :#{_v});) <<
%Q( @block.ctx.__send__("#{_v}=", _check_bool(:#{_v}, bool));) <<
%Q(end)
)
end
# integer config directives for app
{
# config name, minimum value
client_body_buffer_size: 1,
client_header_buffer_size: 1,
client_max_header_size: 1,
client_timeout: 0,
}.each do |_v,minval|
eval(
%Q(def #{_v}(val);) <<
%Q( _check_in_block(:app, :#{_v});) <<
%Q( @block.ctx.__send__("#{_v}=", _check_int(:#{_v}, val, #{minval}));) <<
%Q(end)
)
end
def client_max_body_size(val)
var = _check_in_block(:app, :client_max_body_size)
val = _check_int(var, val, 0) if val != nil
@block.ctx.__send__("#{var}=", val)
end
def input_buffering(val)
var = _check_in_block(:app, :input_buffering)
ok = [ :lazy, true, false ]
ok.include?(val) or
raise ArgumentError, "`#{var}' must be one of: #{ok.inspect}"
@block.ctx.__send__("#{var}=", val)
end
# used to configure rack.errors destination
def errors(val)
var = _check_in_block(:app, :errors)
if String === val
# we've already bound working_directory by the time we get here
val = File.open(File.expand_path(val), "a")
val.close_on_exec = val.sync = true
val.binmode
else
rt = %w(puts write flush).map(&:to_sym) # match Rack::Lint
rt.all? { |m| val.respond_to?(m) } or raise ArgumentError,
"`#{var}' destination must respond to all of: #{rt.inspect}"
end
@block.ctx.__send__("#{var}=", val)
end
def commit!(server)
# redirect IOs
{ stdout_path: $stdout, stderr_path: $stderr }.each do |key, io|
path = @set[key]
if path == :unset && server.daemon_pipe
@set[key] = path = "/dev/null"
end
File.open(path, 'a') { |fp| io.reopen(fp) } if String === path
io.close_on_exec = io.sync = true
end
[ :logger, :pid, :worker_processes ].each do |var|
val = @set[var]
server.__send__("#{var}=", val) if val != :unset
end
queue(:default) if @qeggs.empty?
@app_ctx.each { |app| app.logger ||= server.logger }
end
end
|