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,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,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?(:EventFD)
4
+ require_relative 'sigevent_efd'
5
+ else
6
+ require_relative 'sigevent_pipe'
7
+ 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