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.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/COPYING +674 -0
- data/GIT-VERSION-GEN +41 -0
- data/GNUmakefile +90 -0
- data/README +127 -0
- data/Rakefile +60 -0
- data/bin/yahns +32 -0
- data/examples/README +3 -0
- data/examples/init.sh +76 -0
- data/examples/logger_mp_safe.rb +28 -0
- data/examples/logrotate.conf +32 -0
- data/examples/yahns_multi.conf.rb +89 -0
- data/examples/yahns_rack_basic.conf.rb +27 -0
- data/lib/yahns.rb +73 -0
- data/lib/yahns/acceptor.rb +28 -0
- data/lib/yahns/client_expire.rb +40 -0
- data/lib/yahns/client_expire_portable.rb +39 -0
- data/lib/yahns/config.rb +344 -0
- data/lib/yahns/daemon.rb +51 -0
- data/lib/yahns/fdmap.rb +90 -0
- data/lib/yahns/http_client.rb +198 -0
- data/lib/yahns/http_context.rb +65 -0
- data/lib/yahns/http_response.rb +184 -0
- data/lib/yahns/log.rb +73 -0
- data/lib/yahns/queue.rb +7 -0
- data/lib/yahns/queue_egg.rb +23 -0
- data/lib/yahns/queue_epoll.rb +57 -0
- data/lib/yahns/rack.rb +80 -0
- data/lib/yahns/server.rb +336 -0
- data/lib/yahns/server_mp.rb +181 -0
- data/lib/yahns/sigevent.rb +7 -0
- data/lib/yahns/sigevent_efd.rb +18 -0
- data/lib/yahns/sigevent_pipe.rb +29 -0
- data/lib/yahns/socket_helper.rb +117 -0
- data/lib/yahns/stream_file.rb +34 -0
- data/lib/yahns/stream_input.rb +150 -0
- data/lib/yahns/tee_input.rb +114 -0
- data/lib/yahns/tmpio.rb +27 -0
- data/lib/yahns/wbuf.rb +36 -0
- data/lib/yahns/wbuf_common.rb +32 -0
- data/lib/yahns/worker.rb +58 -0
- data/test/covshow.rb +29 -0
- data/test/helper.rb +115 -0
- data/test/server_helper.rb +65 -0
- data/test/test_bin.rb +97 -0
- data/test/test_client_expire.rb +132 -0
- data/test/test_config.rb +56 -0
- data/test/test_fdmap.rb +19 -0
- data/test/test_output_buffering.rb +291 -0
- data/test/test_queue.rb +59 -0
- data/test/test_rack.rb +28 -0
- data/test/test_serve_static.rb +42 -0
- data/test/test_server.rb +415 -0
- data/test/test_stream_file.rb +30 -0
- data/test/test_wbuf.rb +136 -0
- data/yahns.gemspec +19 -0
- metadata +165 -0
@@ -0,0 +1,181 @@
|
|
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
|
+
module Yahns::ServerMP # :nodoc:
|
5
|
+
EXIT_SIGS = [ :QUIT, :TERM, :INT ]
|
6
|
+
|
7
|
+
def mp_init
|
8
|
+
trap(:CHLD) { @sev.sev_signal }
|
9
|
+
end
|
10
|
+
|
11
|
+
# reaps all unreaped workers
|
12
|
+
def reap_all_workers
|
13
|
+
begin
|
14
|
+
wpid, status = Process.waitpid2(-1, Process::WNOHANG)
|
15
|
+
wpid or return
|
16
|
+
if @reexec_pid == wpid
|
17
|
+
@logger.error "reaped #{status.inspect} exec()-ed"
|
18
|
+
@reexec_pid = 0
|
19
|
+
self.pid = @pid.chomp('.oldbin') if @pid
|
20
|
+
proc_name 'master'
|
21
|
+
else
|
22
|
+
worker = @workers.delete(wpid)
|
23
|
+
worker_id = worker ? worker.nr : "(unknown)"
|
24
|
+
m = "reaped #{status.inspect} worker=#{worker_id}"
|
25
|
+
status.success? ? @logger.info(m) : @logger.error(m)
|
26
|
+
end
|
27
|
+
rescue Errno::ECHILD
|
28
|
+
return
|
29
|
+
end while true
|
30
|
+
end
|
31
|
+
|
32
|
+
def maintain_worker_count
|
33
|
+
(off = @workers.size - @worker_processes) == 0 and return
|
34
|
+
off < 0 and return spawn_missing_workers
|
35
|
+
@workers.each_pair do |wpid, worker|
|
36
|
+
worker.nr >= @worker_processes and Process.kill(:QUIT, wpid)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# delivers a signal to each worker
|
41
|
+
def kill_each_worker(signal)
|
42
|
+
@workers.each_key { |wpid| Process.kill(signal, wpid) }
|
43
|
+
end
|
44
|
+
|
45
|
+
# this is the first thing that runs after forking in a child
|
46
|
+
# gets rid of stuff the worker has no business keeping track of
|
47
|
+
# to free some resources and drops all sig handlers.
|
48
|
+
# traps for USR1, USR2, and HUP may be set in the after_fork Proc
|
49
|
+
# by the user.
|
50
|
+
def after_fork_internal(worker)
|
51
|
+
worker.atfork_child
|
52
|
+
|
53
|
+
# daemon_pipe may be true for non-initial workers
|
54
|
+
@daemon_pipe = @daemon_pipe.close if @daemon_pipe.respond_to?(:close)
|
55
|
+
|
56
|
+
srand # in case this pops up again: https://bugs.ruby-lang.org/issues/4338
|
57
|
+
|
58
|
+
# The OpenSSL PRNG is seeded with only the pid, and apps with frequently
|
59
|
+
# dying workers can recycle pids
|
60
|
+
OpenSSL::Random.seed(rand.to_s) if defined?(OpenSSL::Random)
|
61
|
+
# we'll re-trap EXIT_SIGS later for graceful shutdown iff we accept clients
|
62
|
+
EXIT_SIGS.each { |sig| trap(sig) { exit!(0) } }
|
63
|
+
exit!(0) if (@sig_queue & EXIT_SIGS)[0] # did we inherit sigs from parent?
|
64
|
+
@sig_queue = []
|
65
|
+
|
66
|
+
# ignore WINCH, TTIN, TTOU, HUP in the workers
|
67
|
+
(Yahns::Server::QUEUE_SIGS - EXIT_SIGS).each { |sig| trap(sig, nil) }
|
68
|
+
trap(:CHLD, 'DEFAULT')
|
69
|
+
@logger.info("worker=#{worker.nr} spawned pid=#$$")
|
70
|
+
proc_name "worker[#{worker.nr}]"
|
71
|
+
Yahns::START.clear
|
72
|
+
@sev.close
|
73
|
+
@sev = Yahns::Sigevent.new
|
74
|
+
worker.user(*@user) if @user
|
75
|
+
@user = @workers = nil
|
76
|
+
end
|
77
|
+
|
78
|
+
def spawn_missing_workers
|
79
|
+
worker_nr = -1
|
80
|
+
until (worker_nr += 1) == @worker_processes
|
81
|
+
@workers.value?(worker_nr) and next
|
82
|
+
worker = Yahns::Worker.new(worker_nr)
|
83
|
+
@logger.info("worker=#{worker_nr} spawning...")
|
84
|
+
if pid = fork
|
85
|
+
@workers[pid] = worker.atfork_parent
|
86
|
+
else
|
87
|
+
after_fork_internal(worker)
|
88
|
+
run_mp_worker(worker)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
rescue => e
|
92
|
+
Yahns::Log.exception(@logger, "spawning worker", e)
|
93
|
+
exit!
|
94
|
+
end
|
95
|
+
|
96
|
+
# monitors children and receives signals forever
|
97
|
+
# (or until a termination signal is sent). This handles signals
|
98
|
+
# one-at-a-time time and we'll happily drop signals in case somebody
|
99
|
+
# is signalling us too often.
|
100
|
+
def join
|
101
|
+
spawn_missing_workers
|
102
|
+
state = :respawn # :QUIT, :WINCH
|
103
|
+
proc_name 'master'
|
104
|
+
@logger.info "master process ready"
|
105
|
+
daemon_ready
|
106
|
+
begin
|
107
|
+
@sev.kgio_wait_readable
|
108
|
+
@sev.yahns_step
|
109
|
+
reap_all_workers
|
110
|
+
case @sig_queue.shift
|
111
|
+
when *EXIT_SIGS # graceful shutdown (twice for non graceful)
|
112
|
+
self.listeners = []
|
113
|
+
kill_each_worker(:QUIT)
|
114
|
+
state = :QUIT
|
115
|
+
when :USR1 # rotate logs
|
116
|
+
usr1_reopen("master ")
|
117
|
+
kill_each_worker(:USR1)
|
118
|
+
when :USR2 # exec binary, stay alive in case something went wrong
|
119
|
+
reexec
|
120
|
+
when :WINCH
|
121
|
+
if @daemon_pipe
|
122
|
+
state = :WINCH
|
123
|
+
@logger.info "gracefully stopping all workers"
|
124
|
+
kill_each_worker(:QUIT)
|
125
|
+
@worker_processes = 0
|
126
|
+
else
|
127
|
+
@logger.info "SIGWINCH ignored because we're not daemonized"
|
128
|
+
end
|
129
|
+
when :TTIN
|
130
|
+
state = :respawn unless state == :QUIT
|
131
|
+
@worker_processes += 1
|
132
|
+
when :TTOU
|
133
|
+
@worker_processes -= 1 if @worker_processes > 0
|
134
|
+
when :HUP
|
135
|
+
state = :respawn unless state == :QUIT
|
136
|
+
if @config.config_file
|
137
|
+
load_config!
|
138
|
+
else # exec binary and exit if there's no config file
|
139
|
+
@logger.info "config_file not present, reexecuting binary"
|
140
|
+
reexec
|
141
|
+
end
|
142
|
+
end while @sig_queue[0]
|
143
|
+
maintain_worker_count if state == :respawn
|
144
|
+
rescue => e
|
145
|
+
Yahns::Log.exception(@logger, "master loop error", e)
|
146
|
+
end while state != :QUIT || @workers.size > 0
|
147
|
+
@logger.info "master complete"
|
148
|
+
unlink_pid_safe(@pid) if @pid
|
149
|
+
end
|
150
|
+
|
151
|
+
def fdmap_init_mp
|
152
|
+
fdmap = fdmap_init # builds apps (if not preloading)
|
153
|
+
EXIT_SIGS.each { |sig| trap(sig) { sqwakeup(sig) } }
|
154
|
+
@config.postfork_cleanup # reduce live objects
|
155
|
+
fdmap
|
156
|
+
end
|
157
|
+
|
158
|
+
def run_mp_worker(worker)
|
159
|
+
fdmap = fdmap_init_mp
|
160
|
+
alive = true
|
161
|
+
begin
|
162
|
+
alive = mp_sig_handle(worker, alive)
|
163
|
+
rescue => e
|
164
|
+
Yahns::Log.exception(@logger, "main worker loop", e)
|
165
|
+
end while alive || fdmap.size > 0
|
166
|
+
exit
|
167
|
+
end
|
168
|
+
|
169
|
+
def mp_sig_handle(worker, alive)
|
170
|
+
# not performance critical
|
171
|
+
r = IO.select([worker, @sev], nil, nil, alive ? nil : 0.01) and
|
172
|
+
r[0].each { |io| io.yahns_step }
|
173
|
+
case @sig_queue.shift
|
174
|
+
when *EXIT_SIGS
|
175
|
+
return quit_enter(alive)
|
176
|
+
when :USR1
|
177
|
+
usr1_reopen("worker ")
|
178
|
+
end
|
179
|
+
alive
|
180
|
+
end
|
181
|
+
end
|
@@ -0,0 +1,18 @@
|
|
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::Sigevent < SleepyPenguin::EventFD # :nodoc:
|
5
|
+
include Kgio::DefaultWaiters
|
6
|
+
def self.new
|
7
|
+
super(0, SleepyPenguin::EventFD::CLOEXEC)
|
8
|
+
end
|
9
|
+
|
10
|
+
def sev_signal
|
11
|
+
incr(1) # eventfd_write
|
12
|
+
end
|
13
|
+
|
14
|
+
def yahns_step
|
15
|
+
value(true) # eventfd_read, we ignore this data
|
16
|
+
:wait_readable
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,29 @@
|
|
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::Sigevent # :nodoc:
|
5
|
+
attr_reader :to_io
|
6
|
+
def initialize
|
7
|
+
@to_io, @wr = Kgio::Pipe.new
|
8
|
+
end
|
9
|
+
|
10
|
+
def kgio_wait_readable
|
11
|
+
@to_io.kgio_wait_readable
|
12
|
+
end
|
13
|
+
|
14
|
+
def sev_signal
|
15
|
+
@wr.kgio_write(".")
|
16
|
+
end
|
17
|
+
|
18
|
+
def yahns_step
|
19
|
+
# 11 byte strings -> no malloc on YARV
|
20
|
+
while String === @to_io.kgio_tryread(11)
|
21
|
+
end
|
22
|
+
:wait_readable
|
23
|
+
end
|
24
|
+
|
25
|
+
def close
|
26
|
+
@to_io.close
|
27
|
+
@wr.close
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,117 @@
|
|
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
|
+
# this is only meant for Yahns::Server
|
5
|
+
module Yahns::SocketHelper # :nodoc:
|
6
|
+
def set_server_sockopt(sock, opt)
|
7
|
+
opt = {backlog: 1024}.merge!(opt) if opt
|
8
|
+
|
9
|
+
TCPSocket === sock and sock.setsockopt(:IPPROTO_TCP, :TCP_NODELAY, 1)
|
10
|
+
sock.setsockopt(:SOL_SOCKET, :SO_KEEPALIVE, 1)
|
11
|
+
|
12
|
+
if opt[:rcvbuf] || opt[:sndbuf]
|
13
|
+
log_buffer_sizes(sock, "before: ")
|
14
|
+
{ SO_RCVBUF: :rcvbuf, SO_SNDBUF: :sndbuf }.each do |optname,cfgname|
|
15
|
+
val = opt[cfgname] and sock.setsockopt(:SOL_SOCKET, optname, val)
|
16
|
+
end
|
17
|
+
log_buffer_sizes(sock, " after: ")
|
18
|
+
end
|
19
|
+
sock.listen(opt[:backlog])
|
20
|
+
rescue => e
|
21
|
+
Yahns::Log.exception(@logger, "#{sock_name(sock)} #{opt.inspect}", e)
|
22
|
+
end
|
23
|
+
|
24
|
+
def log_buffer_sizes(sock, pfx = '')
|
25
|
+
rcvbuf = sock.getsockopt(:SOL_SOCKET, :SO_RCVBUF).int
|
26
|
+
sndbuf = sock.getsockopt(:SOL_SOCKET, :SO_SNDBUF).int
|
27
|
+
@logger.info("#{pfx}#{sock_name(sock)} rcvbuf=#{rcvbuf} sndbuf=#{sndbuf}")
|
28
|
+
end
|
29
|
+
|
30
|
+
# creates a new server, socket. address may be a HOST:PORT or
|
31
|
+
# an absolute path to a UNIX socket. address can even be a Socket
|
32
|
+
# object in which case it is immediately returned
|
33
|
+
def bind_listen(address, opt)
|
34
|
+
return address unless String === address
|
35
|
+
opt ||= {}
|
36
|
+
|
37
|
+
sock = if address[0] == ?/
|
38
|
+
if File.exist?(address)
|
39
|
+
if File.socket?(address)
|
40
|
+
begin
|
41
|
+
UNIXSocket.new(address).close
|
42
|
+
# fall through, try to bind(2) and fail with EADDRINUSE
|
43
|
+
# (or succeed from a small race condition we can't sanely avoid).
|
44
|
+
rescue Errno::ECONNREFUSED
|
45
|
+
@logger.info "unlinking existing socket=#{address}"
|
46
|
+
File.unlink(address)
|
47
|
+
end
|
48
|
+
else
|
49
|
+
raise ArgumentError,
|
50
|
+
"socket=#{address} specified but it is not a socket!"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
old_umask = File.umask(opt[:umask] || 0)
|
54
|
+
begin
|
55
|
+
Kgio::UNIXServer.new(address)
|
56
|
+
ensure
|
57
|
+
File.umask(old_umask)
|
58
|
+
end
|
59
|
+
elsif /\A\[([a-fA-F0-9:]+)\]:(\d+)\z/ =~ address
|
60
|
+
new_ipv6_server($1, $2.to_i, opt)
|
61
|
+
elsif /\A(\d+\.\d+\.\d+\.\d+):(\d+)\z/ =~ address
|
62
|
+
Kgio::TCPServer.new($1, $2.to_i)
|
63
|
+
else
|
64
|
+
raise ArgumentError, "Don't know how to bind: #{address}"
|
65
|
+
end
|
66
|
+
set_server_sockopt(sock, opt)
|
67
|
+
sock
|
68
|
+
end
|
69
|
+
|
70
|
+
def new_ipv6_server(addr, port, opt)
|
71
|
+
opt.key?(:ipv6only) or return Kgio::TCPServer.new(addr, port)
|
72
|
+
sock = Socket.new(:AF_INET6, :SOCK_STREAM, 0)
|
73
|
+
sock.setsockopt(:IPPROTO_IPV6, :IPV6_V6ONLY, opt[:ipv6only] ? 1 : 0)
|
74
|
+
sock.setsockopt(:SOL_SOCKET, :SO_REUSEADDR, 1)
|
75
|
+
sock.bind(Socket.pack_sockaddr_in(port, addr))
|
76
|
+
sock.autoclose = false
|
77
|
+
Kgio::TCPServer.for_fd(sock.fileno)
|
78
|
+
end
|
79
|
+
|
80
|
+
# returns rfc2732-style (e.g. "[::1]:666") addresses for IPv6
|
81
|
+
def tcp_name(sock)
|
82
|
+
port, addr = Socket.unpack_sockaddr_in(sock.getsockname)
|
83
|
+
/:/ =~ addr ? "[#{addr}]:#{port}" : "#{addr}:#{port}"
|
84
|
+
end
|
85
|
+
|
86
|
+
# Returns the configuration name of a socket as a string. sock may
|
87
|
+
# be a string value, in which case it is returned as-is
|
88
|
+
# Warning: TCP sockets may not always return the name given to it.
|
89
|
+
def sock_name(sock)
|
90
|
+
case sock
|
91
|
+
when String then sock
|
92
|
+
when UNIXServer
|
93
|
+
Socket.unpack_sockaddr_un(sock.getsockname)
|
94
|
+
when TCPServer
|
95
|
+
tcp_name(sock)
|
96
|
+
when Socket
|
97
|
+
begin
|
98
|
+
tcp_name(sock)
|
99
|
+
rescue ArgumentError
|
100
|
+
Socket.unpack_sockaddr_un(sock.getsockname)
|
101
|
+
end
|
102
|
+
else
|
103
|
+
raise ArgumentError, "Unhandled class #{sock.class}: #{sock.inspect}"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# casts a given Socket to be a TCPServer or UNIXServer
|
108
|
+
def server_cast(sock)
|
109
|
+
sock.autoclose = false
|
110
|
+
begin
|
111
|
+
Socket.unpack_sockaddr_in(sock.getsockname)
|
112
|
+
Kgio::TCPServer.for_fd(sock.fileno)
|
113
|
+
rescue ArgumentError
|
114
|
+
Kgio::UNIXServer.for_fd(sock.fileno)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> et. al.
|
3
|
+
# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
|
4
|
+
require_relative 'wbuf_common'
|
5
|
+
|
6
|
+
class Yahns::StreamFile # :nodoc:
|
7
|
+
include Yahns::WbufCommon
|
8
|
+
|
9
|
+
def initialize(body, persist, offset, count)
|
10
|
+
if body.respond_to?(:to_io)
|
11
|
+
@tmpio = body.to_io
|
12
|
+
else
|
13
|
+
path = body.to_path
|
14
|
+
if path =~ %r{\A/dev/fd/(\d+)\z}
|
15
|
+
@tmpio = IO.for_fd($1.to_i)
|
16
|
+
@tmpio.autoclose = false
|
17
|
+
else
|
18
|
+
@tmpio = File.open(path)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
@sf_offset = offset
|
22
|
+
@sf_count = count || @tmpio.stat.size
|
23
|
+
@wbuf_persist = persist # whether or not we keep the connection alive
|
24
|
+
@body = body
|
25
|
+
end
|
26
|
+
|
27
|
+
# called by last wbuf_flush
|
28
|
+
def wbuf_close(client)
|
29
|
+
if File === @tmpio && @tmpio != @body
|
30
|
+
@tmpio.close
|
31
|
+
end
|
32
|
+
wbuf_close_common(client)
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
# -*- encoding: binary -*-
|
2
|
+
# Copyright (C) 2009-2013, Eric Wong <normalperson@yhbt.net> et. al.
|
3
|
+
# License: GPLv2 or later (https://www.gnu.org/licenses/gpl-2.0.txt)
|
4
|
+
|
5
|
+
# When processing uploads, Yahns may expose a StreamInput object under
|
6
|
+
# "rack.input" of the (future) Rack (2.x) environment.
|
7
|
+
class Yahns::StreamInput # :nodoc:
|
8
|
+
# Initializes a new StreamInput object. You normally do not have to call
|
9
|
+
# this unless you are writing an HTTP server.
|
10
|
+
def initialize(client, request)
|
11
|
+
@chunked = request.content_length.nil?
|
12
|
+
@client = client
|
13
|
+
@parser = request
|
14
|
+
@buf = request.buf
|
15
|
+
@rbuf = ''
|
16
|
+
@bytes_read = 0
|
17
|
+
filter_body(@rbuf, @buf) unless @buf.empty?
|
18
|
+
end
|
19
|
+
|
20
|
+
# :call-seq:
|
21
|
+
# ios.read([length [, buffer ]]) => string, buffer, or nil
|
22
|
+
#
|
23
|
+
# Reads at most length bytes from the I/O stream, or to the end of
|
24
|
+
# file if length is omitted or is nil. length must be a non-negative
|
25
|
+
# integer or nil. If the optional buffer argument is present, it
|
26
|
+
# must reference a String, which will receive the data.
|
27
|
+
#
|
28
|
+
# At end of file, it returns nil or '' depend on length.
|
29
|
+
# ios.read() and ios.read(nil) returns ''.
|
30
|
+
# ios.read(length [, buffer]) returns nil.
|
31
|
+
#
|
32
|
+
# If the Content-Length of the HTTP request is known (as is the common
|
33
|
+
# case for POST requests), then ios.read(length [, buffer]) will block
|
34
|
+
# until the specified length is read (or it is the last chunk).
|
35
|
+
# Otherwise, for uncommon "Transfer-Encoding: chunked" requests,
|
36
|
+
# ios.read(length [, buffer]) will return immediately if there is
|
37
|
+
# any data and only block when nothing is available (providing
|
38
|
+
# IO#readpartial semantics).
|
39
|
+
def read(length = nil, rv = '')
|
40
|
+
if length
|
41
|
+
if length <= @rbuf.size
|
42
|
+
length < 0 and raise ArgumentError, "negative length #{length} given"
|
43
|
+
rv.replace(@rbuf.slice!(0, length))
|
44
|
+
else
|
45
|
+
to_read = length - @rbuf.size
|
46
|
+
rv.replace(@rbuf.slice!(0, @rbuf.size))
|
47
|
+
until to_read == 0 || eof? || (rv.size > 0 && @chunked)
|
48
|
+
@client.kgio_read(to_read, @buf) or eof!
|
49
|
+
filter_body(@rbuf, @buf)
|
50
|
+
rv << @rbuf
|
51
|
+
to_read -= @rbuf.size
|
52
|
+
end
|
53
|
+
@rbuf.replace('')
|
54
|
+
end
|
55
|
+
rv = nil if rv.empty? && length != 0
|
56
|
+
else
|
57
|
+
read_all(rv)
|
58
|
+
end
|
59
|
+
rv
|
60
|
+
end
|
61
|
+
|
62
|
+
def __rsize
|
63
|
+
@client.class.client_body_buffer_size
|
64
|
+
end
|
65
|
+
|
66
|
+
# :call-seq:
|
67
|
+
# ios.gets => string or nil
|
68
|
+
#
|
69
|
+
# Reads the next ``line'' from the I/O stream; lines are separated
|
70
|
+
# by the global record separator ($/, typically "\n"). A global
|
71
|
+
# record separator of nil reads the entire unread contents of ios.
|
72
|
+
# Returns nil if called at the end of file.
|
73
|
+
# This takes zero arguments for strict Rack::Lint compatibility,
|
74
|
+
# unlike IO#gets.
|
75
|
+
def gets
|
76
|
+
sep = $/
|
77
|
+
if sep.nil?
|
78
|
+
read_all(rv = '')
|
79
|
+
return rv.empty? ? nil : rv
|
80
|
+
end
|
81
|
+
re = /\A(.*?#{Regexp.escape(sep)})/
|
82
|
+
rsize = __rsize
|
83
|
+
begin
|
84
|
+
@rbuf.sub!(re, '') and return $1
|
85
|
+
return @rbuf.empty? ? nil : @rbuf.slice!(0, @rbuf.size) if eof?
|
86
|
+
@client.kgio_read(rsize, @buf) or eof!
|
87
|
+
filter_body(once = '', @buf)
|
88
|
+
@rbuf << once
|
89
|
+
end while true
|
90
|
+
end
|
91
|
+
|
92
|
+
# :call-seq:
|
93
|
+
# ios.each { |line| block } => ios
|
94
|
+
#
|
95
|
+
# Executes the block for every ``line'' in *ios*, where lines are
|
96
|
+
# separated by the global record separator ($/, typically "\n").
|
97
|
+
def each
|
98
|
+
while line = gets
|
99
|
+
yield line
|
100
|
+
end
|
101
|
+
|
102
|
+
self # Rack does not specify what the return value is here
|
103
|
+
end
|
104
|
+
|
105
|
+
def eof?
|
106
|
+
if @parser.body_eof?
|
107
|
+
rsize = __rsize
|
108
|
+
while @chunked && ! @parser.parse
|
109
|
+
once = @client.kgio_read(rsize) or eof!
|
110
|
+
@buf << once
|
111
|
+
end
|
112
|
+
@client = nil
|
113
|
+
true
|
114
|
+
else
|
115
|
+
false
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def filter_body(dst, src)
|
120
|
+
rv = @parser.filter_body(dst, src)
|
121
|
+
@bytes_read += dst.size
|
122
|
+
rv
|
123
|
+
end
|
124
|
+
|
125
|
+
def read_all(dst)
|
126
|
+
dst.replace(@rbuf)
|
127
|
+
@client or return
|
128
|
+
rsize = @client.class.client_body_buffer_size
|
129
|
+
until eof?
|
130
|
+
@client.kgio_read(rsize, @buf) or eof!
|
131
|
+
filter_body(@rbuf, @buf)
|
132
|
+
dst << @rbuf
|
133
|
+
end
|
134
|
+
ensure
|
135
|
+
@rbuf.replace('')
|
136
|
+
end
|
137
|
+
|
138
|
+
def eof!
|
139
|
+
# in case client only did a premature shutdown(SHUT_WR)
|
140
|
+
# we do support clients that shutdown(SHUT_WR) after the
|
141
|
+
# _entire_ request has been sent, and those will not have
|
142
|
+
# raised EOFError on us.
|
143
|
+
@client.shutdown if @client
|
144
|
+
ensure
|
145
|
+
raise Yahns::ClientShutdown, "bytes_read=#{@bytes_read}", []
|
146
|
+
end
|
147
|
+
|
148
|
+
def close # return nil
|
149
|
+
end
|
150
|
+
end
|