yahns 0.0.0TP1

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