yahns 0.0.0TP1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|