yahns 0.0.0TP1

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 (58) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/COPYING +674 -0
  4. data/GIT-VERSION-GEN +41 -0
  5. data/GNUmakefile +90 -0
  6. data/README +127 -0
  7. data/Rakefile +60 -0
  8. data/bin/yahns +32 -0
  9. data/examples/README +3 -0
  10. data/examples/init.sh +76 -0
  11. data/examples/logger_mp_safe.rb +28 -0
  12. data/examples/logrotate.conf +32 -0
  13. data/examples/yahns_multi.conf.rb +89 -0
  14. data/examples/yahns_rack_basic.conf.rb +27 -0
  15. data/lib/yahns.rb +73 -0
  16. data/lib/yahns/acceptor.rb +28 -0
  17. data/lib/yahns/client_expire.rb +40 -0
  18. data/lib/yahns/client_expire_portable.rb +39 -0
  19. data/lib/yahns/config.rb +344 -0
  20. data/lib/yahns/daemon.rb +51 -0
  21. data/lib/yahns/fdmap.rb +90 -0
  22. data/lib/yahns/http_client.rb +198 -0
  23. data/lib/yahns/http_context.rb +65 -0
  24. data/lib/yahns/http_response.rb +184 -0
  25. data/lib/yahns/log.rb +73 -0
  26. data/lib/yahns/queue.rb +7 -0
  27. data/lib/yahns/queue_egg.rb +23 -0
  28. data/lib/yahns/queue_epoll.rb +57 -0
  29. data/lib/yahns/rack.rb +80 -0
  30. data/lib/yahns/server.rb +336 -0
  31. data/lib/yahns/server_mp.rb +181 -0
  32. data/lib/yahns/sigevent.rb +7 -0
  33. data/lib/yahns/sigevent_efd.rb +18 -0
  34. data/lib/yahns/sigevent_pipe.rb +29 -0
  35. data/lib/yahns/socket_helper.rb +117 -0
  36. data/lib/yahns/stream_file.rb +34 -0
  37. data/lib/yahns/stream_input.rb +150 -0
  38. data/lib/yahns/tee_input.rb +114 -0
  39. data/lib/yahns/tmpio.rb +27 -0
  40. data/lib/yahns/wbuf.rb +36 -0
  41. data/lib/yahns/wbuf_common.rb +32 -0
  42. data/lib/yahns/worker.rb +58 -0
  43. data/test/covshow.rb +29 -0
  44. data/test/helper.rb +115 -0
  45. data/test/server_helper.rb +65 -0
  46. data/test/test_bin.rb +97 -0
  47. data/test/test_client_expire.rb +132 -0
  48. data/test/test_config.rb +56 -0
  49. data/test/test_fdmap.rb +19 -0
  50. data/test/test_output_buffering.rb +291 -0
  51. data/test/test_queue.rb +59 -0
  52. data/test/test_rack.rb +28 -0
  53. data/test/test_serve_static.rb +42 -0
  54. data/test/test_server.rb +415 -0
  55. data/test/test_stream_file.rb +30 -0
  56. data/test/test_wbuf.rb +136 -0
  57. data/yahns.gemspec +19 -0
  58. metadata +165 -0
@@ -0,0 +1,73 @@
1
+ # -*- encoding: binary -*-
2
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
3
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
4
+
5
+ # logging-related utility functions for all of yahns
6
+ module Yahns::Log # :nodoc:
7
+ def self.exception(logger, prefix, exc)
8
+ message = exc.message
9
+ message = message.dump if /[[:cntrl:]]/ =~ message # prevent code injection
10
+ logger.error "#{prefix}: #{message} (#{exc.class})"
11
+ exc.backtrace.each { |line| logger.error(line) }
12
+ end
13
+
14
+ def self.is_log?(fp)
15
+ append_flags = IO::WRONLY | IO::APPEND
16
+
17
+ ! fp.closed? &&
18
+ fp.stat.file? &&
19
+ fp.sync &&
20
+ (fp.fcntl(Fcntl::F_GETFL) & append_flags) == append_flags
21
+ rescue IOError, Errno::EBADF
22
+ false
23
+ end
24
+
25
+ def self.chown_all(uid, gid)
26
+ ObjectSpace.each_object(File) do |fp|
27
+ fp.chown(uid, gid) if is_log?(fp)
28
+ end
29
+ end
30
+
31
+ # This reopens ALL logfiles in the process that have been rotated
32
+ # using logrotate(8) (without copytruncate) or similar tools.
33
+ # A +File+ object is considered for reopening if it is:
34
+ # 1) opened with the O_APPEND and O_WRONLY flags
35
+ # 2) the current open file handle does not match its original open path
36
+ # 3) unbuffered (as far as userspace buffering goes, not O_SYNC)
37
+ # Returns the number of files reopened
38
+ def self.reopen_all
39
+ to_reopen = []
40
+ nr = 0
41
+ ObjectSpace.each_object(File) { |fp| is_log?(fp) and to_reopen << fp }
42
+
43
+ to_reopen.each do |fp|
44
+ orig_st = begin
45
+ fp.stat
46
+ rescue IOError, Errno::EBADF
47
+ next
48
+ end
49
+
50
+ begin
51
+ b = File.stat(fp.path)
52
+ next if orig_st.ino == b.ino && orig_st.dev == b.dev
53
+ rescue Errno::ENOENT
54
+ end
55
+
56
+ begin
57
+ File.open(fp.path, 'a') { |tmpfp| fp.reopen(tmpfp) }
58
+ fp.sync = true
59
+ new_st = fp.stat
60
+
61
+ # this should only happen in the master:
62
+ if orig_st.uid != new_st.uid || orig_st.gid != new_st.gid
63
+ fp.chown(orig_st.uid, orig_st.gid)
64
+ end
65
+
66
+ nr += 1
67
+ rescue IOError, Errno::EBADF
68
+ # not much we can do...
69
+ end
70
+ end
71
+ nr
72
+ end
73
+ end
@@ -0,0 +1,7 @@
1
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net>
2
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
3
+ if SleepyPenguin.const_defined?(:Epoll)
4
+ require_relative 'queue_epoll'
5
+ else
6
+ require_relative 'queue_kqueue' # TODO
7
+ end
@@ -0,0 +1,23 @@
1
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
2
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
3
+
4
+ # this represents a Yahns::Queue before its vivified. This only
5
+ # lives in the parent process and should be clobbered after qc_vivify
6
+ class Yahns::QueueEgg # :nodoc:
7
+ attr_writer :max_events, :worker_threads
8
+ attr_accessor :logger
9
+
10
+ def initialize
11
+ @max_events = 1 # 1 is good if worker_threads > 1
12
+ @worker_threads = 7 # any default is wrong for most apps...
13
+ @logger = nil
14
+ end
15
+
16
+ # only call after forking
17
+ def qc_vivify(fdmap)
18
+ queue = Yahns::Queue.new
19
+ queue.fdmap = fdmap
20
+ queue.spawn_worker_threads(@logger, @worker_threads, @max_events)
21
+ queue
22
+ end
23
+ end
@@ -0,0 +1,57 @@
1
+ # -*- encoding: binary -*-
2
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
3
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
4
+ class Yahns::Queue < SleepyPenguin::Epoll::IO # :nodoc:
5
+ include SleepyPenguin
6
+ attr_accessor :fdmap # Yahns::Fdmap
7
+
8
+ # public
9
+ QEV_RD = Epoll::IN | Epoll::ONESHOT
10
+ QEV_WR = Epoll::OUT | Epoll::ONESHOT
11
+ QEV_RDWR = QEV_RD | QEV_WR
12
+
13
+ def self.new
14
+ super(SleepyPenguin::Epoll::CLOEXEC)
15
+ end
16
+
17
+ # for HTTP and HTTPS servers, we rely on the io writing to us, first
18
+ # flags: QEV_RD/QEV_WR (usually QEV_RD)
19
+ def queue_add(io, flags)
20
+ @fdmap.add(io)
21
+ epoll_ctl(Epoll::CTL_ADD, io, flags)
22
+ end
23
+
24
+ # returns an array of infinitely running threads
25
+ def spawn_worker_threads(logger, worker_threads, max_events)
26
+ worker_threads.times do
27
+ Thread.new do
28
+ Thread.current[:yahns_rbuf] = ""
29
+ begin
30
+ epoll_wait(max_events) do |_, io| # don't care for flags for now
31
+ case rv = io.yahns_step
32
+ when :wait_readable
33
+ epoll_ctl(Epoll::CTL_MOD, io, QEV_RD)
34
+ when :wait_writable
35
+ epoll_ctl(Epoll::CTL_MOD, io, QEV_WR)
36
+ when :wait_readwrite
37
+ epoll_ctl(Epoll::CTL_MOD, io, QEV_RDWR)
38
+ when :delete # only used by rack.hijack
39
+ epoll_ctl(Epoll::CTL_DEL, io, 0)
40
+ @fdmap.delete(io)
41
+ when nil
42
+ # this is be the ONLY place where we call IO#close on
43
+ # things inside the queue
44
+ io.close
45
+ @fdmap.decr
46
+ else
47
+ raise "BUG: #{io.inspect}#yahns_step returned: #{rv.inspect}"
48
+ end
49
+ end
50
+ rescue => e
51
+ break if (IOError === e || Errno::EBADF === e) && closed?
52
+ Yahns::Log.exception(logger, 'queue loop', e)
53
+ end while true
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,80 @@
1
+ # -*- encoding: binary -*-
2
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
3
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
4
+ require 'rack'
5
+ class Yahns::Rack # :nodoc:
6
+ attr_reader :preload
7
+
8
+ # enforce a single instance for the identical config.ru
9
+ def self.instance_key(*args)
10
+ ru = args[0]
11
+
12
+ # it's safe to expand_path now since we enforce working_directory in the
13
+ # top-level config is called before any apps are created
14
+ # ru may also be a Rack::Builder object or any already-built Rack app
15
+ ru.respond_to?(:call) ? ru.object_id : File.expand_path(ru)
16
+ end
17
+
18
+ def initialize(ru, opts = {})
19
+ # always called after config file parsing, may be called after forking
20
+ @app = lambda do
21
+ ENV["RACK_ENV"] ||= "none"
22
+ if ru.respond_to?(:call)
23
+ inner_app = ru.respond_to?(:to_app) ? ru.to_app : ru
24
+ else
25
+ inner_app = case ru
26
+ when /\.ru$/
27
+ raw = File.read(ru)
28
+ raw.sub!(/^__END__\n.*/, '')
29
+ eval("Rack::Builder.new {(\n#{raw}\n)}.to_app", TOPLEVEL_BINDING, ru)
30
+ else
31
+ require ru
32
+ Object.const_get(File.basename(ru, '.rb').capitalize)
33
+ end
34
+ end
35
+ inner_app
36
+ end
37
+ @ru = ru
38
+ @preload = opts[:preload]
39
+ build_app! if @preload
40
+ end
41
+
42
+ def config_context
43
+ ctx_class = Class.new(Yahns::HttpClient)
44
+ ctx_class.extend(Yahns::HttpContext)
45
+ ctx_class.http_ctx_init(self)
46
+ ctx_class
47
+ end
48
+
49
+ def build_app!
50
+ if @app.respond_to?(:arity) && @app.arity == 0
51
+ Gem.refresh if defined?(Gem) && Gem.respond_to?(:refresh)
52
+ @app = @app.call
53
+ end
54
+ end
55
+
56
+ # allow different HttpContext instances to have different Rack defaults
57
+ def app_defaults
58
+ {
59
+ # logger is set in http_context
60
+ "rack.errors" => $stderr,
61
+ "rack.multiprocess" => true,
62
+ "rack.multithread" => true,
63
+ "rack.run_once" => false,
64
+ "rack.hijack?" => true,
65
+ "rack.version" => [ 1, 2 ],
66
+ "SCRIPT_NAME" => "",
67
+
68
+ # this is not in the Rack spec, but some apps may rely on it
69
+ "SERVER_SOFTWARE" => "yahns"
70
+ }
71
+ end
72
+
73
+ def app_after_fork
74
+ build_app! unless @preload
75
+ @app
76
+ end
77
+ end
78
+
79
+ # register ourselves
80
+ Yahns::Config::APP_CLASS[:rack] = Yahns::Rack
@@ -0,0 +1,336 @@
1
+ # -*- encoding: binary -*-
2
+ # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
3
+ # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
4
+ class Yahns::Server # :nodoc:
5
+ QUEUE_SIGS = [ :WINCH, :QUIT, :INT, :TERM, :USR1, :USR2, :HUP, :TTIN, :TTOU ]
6
+ attr_accessor :daemon_pipe
7
+ attr_accessor :logger
8
+ attr_writer :worker_processes
9
+ include Yahns::SocketHelper
10
+
11
+ def initialize(config)
12
+ @reexec_pid = 0
13
+ @daemon_pipe = nil # writable IO or true
14
+ @config = config
15
+ @workers = {} # pid -> workers
16
+ @sig_queue = [] # nil in forked workers
17
+ @logger = Logger.new($stderr)
18
+ @sev = Yahns::Sigevent.new
19
+ @listeners = []
20
+ @pid = nil
21
+ @worker_processes = nil
22
+ @user = nil
23
+ end
24
+
25
+ def sqwakeup(sig)
26
+ @sig_queue << sig
27
+ @sev.sev_signal
28
+ end
29
+
30
+ def start
31
+ @config.commit!(self)
32
+ inherit_listeners!
33
+ # we try inheriting listeners first, so we bind them later.
34
+ # we don't write the pid file until we've bound listeners in case
35
+ # yahns was started twice by mistake. Even though our #pid= method
36
+ # checks for stale/existing pid files, race conditions are still
37
+ # possible (and difficult/non-portable to avoid) and can be likely
38
+ # to clobber the pid if the second start was in quick succession
39
+ # after the first, so we rely on the listener binding to fail in
40
+ # that case. Some tests (in and outside of this source tree) and
41
+ # monitoring tools may also rely on pid files existing before we
42
+ # attempt to connect to the listener(s)
43
+
44
+ # setup signal handlers before writing pid file in case people get
45
+ # trigger happy and send signals as soon as the pid file exists.
46
+ QUEUE_SIGS.each { |sig| trap(sig) { sqwakeup(sig) } }
47
+ self.pid = @config.value(:pid) # write pid file
48
+ bind_new_listeners!
49
+ if @worker_processes
50
+ require 'yahns/server_mp'
51
+ extend Yahns::ServerMP
52
+ mp_init
53
+ end
54
+ self
55
+ end
56
+
57
+ # replaces current listener set with +listeners+. This will
58
+ # close the socket if it will not exist in the new listener set
59
+ def listeners=(listeners)
60
+ cur_names, dead_names = [], []
61
+ listener_names.each do |name|
62
+ if ?/ == name[0]
63
+ # mark unlinked sockets as dead so we can rebind them
64
+ (File.socket?(name) ? cur_names : dead_names) << name
65
+ else
66
+ cur_names << name
67
+ end
68
+ end
69
+ set_names = listener_names(listeners)
70
+ dead_names.concat(cur_names - set_names).uniq!
71
+
72
+ @listeners.delete_if do |io|
73
+ if dead_names.include?(sock_name(io))
74
+ (io.close rescue nil).nil? # true
75
+ else
76
+ set_server_sockopt(io, sock_opts(io))
77
+ false
78
+ end
79
+ end
80
+
81
+ (set_names - cur_names).each { |addr| listen(addr) }
82
+ end
83
+
84
+ # sets the path for the PID file of the master process
85
+ def pid=(path)
86
+ if path
87
+ if x = valid_pid?(path)
88
+ return path if @pid && path == @pid && x == $$
89
+ if x == @reexec_pid && @pid =~ /\.oldbin\z/
90
+ @logger.warn("will not set pid=#{path} while reexec-ed "\
91
+ "child is running PID:#{x}")
92
+ return
93
+ end
94
+ raise ArgumentError, "Already running on PID:#{x} " \
95
+ "(or pid=#{path} is stale)"
96
+ end
97
+ end
98
+ unlink_pid_safe(@pid) if @pid
99
+
100
+ if path
101
+ fp = begin
102
+ tmp = "#{File.dirname(path)}/#{rand}.#$$"
103
+ File.open(tmp, File::RDWR|File::CREAT|File::EXCL, 0644)
104
+ rescue Errno::EEXIST
105
+ retry
106
+ end
107
+ fp.syswrite("#$$\n")
108
+ File.rename(fp.path, path)
109
+ fp.close
110
+ end
111
+ @pid = path
112
+ end
113
+
114
+ # add a given address to the +listeners+ set, idempotently
115
+ # Allows workers to add a private, per-process listener via the
116
+ # after_fork hook. Very useful for debugging and testing.
117
+ # +:tries+ may be specified as an option for the number of times
118
+ # to retry, and +:delay+ may be specified as the time in seconds
119
+ # to delay between retries.
120
+ # A negative value for +:tries+ indicates the listen will be
121
+ # retried indefinitely, this is useful when workers belonging to
122
+ # different masters are spawned during a transparent upgrade.
123
+ def listen(address)
124
+ address = @config.expand_addr(address)
125
+ return if String === address && listener_names.include?(address)
126
+
127
+ begin
128
+ io = bind_listen(address, sock_opts(address))
129
+ unless Kgio::TCPServer === io || Kgio::UNIXServer === io
130
+ io = server_cast(io)
131
+ end
132
+ @logger.info "listening on addr=#{sock_name(io)} fd=#{io.fileno}"
133
+ @listeners << io
134
+ io
135
+ rescue Errno::EADDRINUSE => err
136
+ @logger.error "adding listener failed addr=#{address} (in use)"
137
+ rescue => err
138
+ @logger.fatal "error adding listener addr=#{address}"
139
+ raise err
140
+ end
141
+ end
142
+
143
+ def daemon_ready
144
+ @daemon_pipe or return
145
+ @daemon_pipe.syswrite("#$$")
146
+ @daemon_pipe.close
147
+ @daemon_pipe = true # for SIGWINCH
148
+ end
149
+
150
+ # reexecutes the Yahns::START with a new binary
151
+ def reexec
152
+ if @reexec_pid > 0
153
+ begin
154
+ Process.kill(0, @reexec_pid)
155
+ @logger.error "reexec-ed child already running PID:#@reexec_pid"
156
+ return
157
+ rescue Errno::ESRCH
158
+ @reexec_pid = 0
159
+ end
160
+ end
161
+
162
+ if @pid
163
+ old_pid = "#@pid.oldbin"
164
+ begin
165
+ self.pid = old_pid # clear the path for a new pid file
166
+ rescue ArgumentError
167
+ @logger.error "old PID:#{valid_pid?(old_pid)} running with " \
168
+ "existing pid=#{old_pid}, refusing rexec"
169
+ return
170
+ rescue => e
171
+ @logger.error "error writing pid=#{old_pid} #{e.class} #{e.message}"
172
+ return
173
+ end
174
+ end
175
+
176
+ @reexec_pid = fork do
177
+ redirects = {}
178
+ listeners.each do |sock|
179
+ sock.close_on_exec = false
180
+ redirects[sock.fileno] = sock
181
+ end
182
+ ENV['YAHNS_FD'] = redirects.keys.map(&:to_s).join(',')
183
+ Dir.chdir(@config.value(:working_directory) || Yahns::START[:cwd])
184
+ cmd = [ Yahns::START[0] ].concat(Yahns::START[:argv])
185
+ @logger.info "executing #{cmd.inspect} (in #{Dir.pwd})"
186
+ cmd << redirects
187
+ exec(*cmd)
188
+ end
189
+ proc_name 'master (old)'
190
+ end
191
+
192
+ # unlinks a PID file at given +path+ if it contains the current PID
193
+ # still potentially racy without locking the directory (which is
194
+ # non-portable and may interact badly with other programs), but the
195
+ # window for hitting the race condition is small
196
+ def unlink_pid_safe(path)
197
+ (File.read(path).to_i == $$ and File.unlink(path)) rescue nil
198
+ end
199
+
200
+ # returns a PID if a given path contains a non-stale PID file,
201
+ # nil otherwise.
202
+ def valid_pid?(path)
203
+ wpid = File.read(path).to_i
204
+ wpid <= 0 and return
205
+ Process.kill(0, wpid)
206
+ wpid
207
+ rescue Errno::EPERM
208
+ @logger.info "pid=#{path} possibly stale, got EPERM signalling PID:#{wpid}"
209
+ nil
210
+ rescue Errno::ESRCH, Errno::ENOENT
211
+ # don't unlink stale pid files, racy without non-portable locking...
212
+ end
213
+
214
+ def load_config!
215
+ @logger.info "reloading config_file=#{@config.config_file}"
216
+ @config.config_reload!
217
+ @config.commit!(self)
218
+ kill_each_worker(:QUIT)
219
+ Yahns::Log.reopen_all
220
+ @logger.info "done reloading config_file=#{@config.config_file}"
221
+ rescue StandardError, LoadError, SyntaxError => e
222
+ Yahns::Log.exception(@logger,
223
+ "error reloading config_file=#{@config.config_file}", e)
224
+ end
225
+
226
+ # returns an array of string names for the given listener array
227
+ def listener_names(listeners = @listeners)
228
+ listeners.map { |io| sock_name(io) }
229
+ end
230
+
231
+ def sock_opts(io)
232
+ @config.config_listeners[sock_name(io)]
233
+ end
234
+
235
+ def inherit_listeners!
236
+ # inherit sockets from parents, they need to be plain Socket objects
237
+ # before they become Kgio::UNIXServer or Kgio::TCPServer
238
+ inherited = ENV['YAHNS_FD'].to_s.split(/,/).map do |fd|
239
+ io = Socket.for_fd(fd.to_i)
240
+ set_server_sockopt(io, sock_opts(io))
241
+ @logger.info "inherited addr=#{sock_name(io)} fd=#{fd}"
242
+ server_cast(io)
243
+ end
244
+
245
+ @listeners.replace(inherited)
246
+ end
247
+
248
+ # call only after calling inherit_listeners!
249
+ # This binds any listeners we did NOT inherit from the parent
250
+ def bind_new_listeners!
251
+ self.listeners = @config.config_listeners.keys
252
+ raise ArgumentError, "no listeners" if @listeners.empty?
253
+ @listeners.each { |l| l.extend(Yahns::Acceptor) }
254
+ end
255
+
256
+ def proc_name(tag)
257
+ s = Yahns::START
258
+ $0 = ([ File.basename(s[0]), tag ]).concat(s[:argv]).join(' ')
259
+ end
260
+
261
+ # spins up processing threads of the server
262
+ def fdmap_init
263
+ thresh = @config.value(:client_expire_threshold)
264
+
265
+ # keeps track of all connections, like ObjectSpace, but only for IOs
266
+ fdmap = Yahns::Fdmap.new(@logger, thresh)
267
+
268
+ # initialize queues (epoll/kqueue) and associated worker threads
269
+ queues = {}
270
+ @config.qeggs.each do |name, qegg|
271
+ queue = qegg.qc_vivify(fdmap) # worker threads run after this
272
+ queues[qegg] = queue
273
+ end
274
+
275
+ # spin up applications (which are preload: false)
276
+ @config.app_ctx.each { |ctx| ctx.after_fork_init }
277
+
278
+ # spin up acceptors, clients flow into worker queues after this
279
+ @listeners.each do |l|
280
+ ctx = sock_opts(l)[:yahns_app_ctx]
281
+ qegg = ctx.qegg || @config.qeggs[:default]
282
+
283
+ # acceptors feed the the queues
284
+ l.spawn_acceptor(@logger, ctx, queues[qegg])
285
+ end
286
+ fdmap
287
+ end
288
+
289
+ def usr1_reopen(prefix)
290
+ @logger.info "#{prefix}reopening logs..."
291
+ Yahns::Log.reopen_all
292
+ @logger.info "#{prefix}done reopening logs"
293
+ end
294
+
295
+ def quit_enter(alive)
296
+ self.listeners = []
297
+ exit(0) unless alive # drop connections immediately if signaled twice
298
+ @config.config_listeners.each_value do |opts|
299
+ ctx = opts[:yahns_app_ctx] or next
300
+ ctx.persistent_connections = false # Yahns::HttpContext
301
+ end
302
+ false
303
+ end
304
+
305
+ def sp_sig_handle(alive)
306
+ @sev.kgio_wait_readable(alive ? nil : 0.01)
307
+ @sev.yahns_step
308
+ case sig = @sig_queue.shift
309
+ when :QUIT, :TERM, :INT
310
+ return quit_enter(alive)
311
+ when :USR1
312
+ usr1_reopen('')
313
+ when :USR2
314
+ reexec
315
+ when :HUP
316
+ reexec
317
+ return false
318
+ when :TTIN, :TTOU, :WINCH
319
+ @logger.info("SIG#{sig} ignored in single-process mode")
320
+ end
321
+ alive
322
+ end
323
+
324
+ # single-threaded only, this is overriden if @worker_processes is non-nil
325
+ def join
326
+ daemon_ready
327
+ fdmap = fdmap_init
328
+ alive = true
329
+ begin
330
+ alive = sp_sig_handle(alive)
331
+ rescue => e
332
+ Yahns::Log.exception(@logger, "main loop", e)
333
+ end while alive || fdmap.size > 0
334
+ unlink_pid_safe(@pid) if @pid
335
+ end
336
+ end