From ab067831e707b191d6dfdcd01de1f1d85fc90d05 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Fri, 18 Oct 2013 10:28:18 +0000 Subject: initial commit --- lib/yahns/config.rb | 341 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 341 insertions(+) create mode 100644 lib/yahns/config.rb (limited to 'lib/yahns/config.rb') diff --git a/lib/yahns/config.rb b/lib/yahns/config.rb new file mode 100644 index 0000000..1d4d110 --- /dev/null +++ b/lib/yahns/config.rb @@ -0,0 +1,341 @@ +# -*- encoding: binary -*- +# Copyright (C) 2013, Eric Wong 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 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) + val > 0 or raise ArgumentError, "#{var} must be > 0" + 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_max_body_size: 0, + 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 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.binmode + val.sync = true + 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.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? + @qeggs.each_value { |qegg| qegg.logger ||= server.logger } + @app_ctx.each { |app| app.logger ||= server.logger } + end +end -- cgit v1.2.3-24-ge0c7