pitchfork 0.1.0

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.

Potentially problematic release.


This version of pitchfork might be problematic. Click here for more details.

Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/.git-blame-ignore-revs +3 -0
  3. data/.gitattributes +5 -0
  4. data/.github/workflows/ci.yml +30 -0
  5. data/.gitignore +23 -0
  6. data/COPYING +674 -0
  7. data/Dockerfile +4 -0
  8. data/Gemfile +9 -0
  9. data/Gemfile.lock +30 -0
  10. data/LICENSE +67 -0
  11. data/README.md +123 -0
  12. data/Rakefile +72 -0
  13. data/docs/Application_Timeouts.md +74 -0
  14. data/docs/CONFIGURATION.md +388 -0
  15. data/docs/DESIGN.md +86 -0
  16. data/docs/FORK_SAFETY.md +80 -0
  17. data/docs/PHILOSOPHY.md +90 -0
  18. data/docs/REFORKING.md +113 -0
  19. data/docs/SIGNALS.md +38 -0
  20. data/docs/TUNING.md +106 -0
  21. data/examples/constant_caches.ru +43 -0
  22. data/examples/echo.ru +25 -0
  23. data/examples/hello.ru +5 -0
  24. data/examples/nginx.conf +156 -0
  25. data/examples/pitchfork.conf.minimal.rb +5 -0
  26. data/examples/pitchfork.conf.rb +77 -0
  27. data/examples/unicorn.socket +11 -0
  28. data/exe/pitchfork +116 -0
  29. data/ext/pitchfork_http/CFLAGS +13 -0
  30. data/ext/pitchfork_http/c_util.h +116 -0
  31. data/ext/pitchfork_http/child_subreaper.h +25 -0
  32. data/ext/pitchfork_http/common_field_optimization.h +130 -0
  33. data/ext/pitchfork_http/epollexclusive.h +124 -0
  34. data/ext/pitchfork_http/ext_help.h +38 -0
  35. data/ext/pitchfork_http/extconf.rb +14 -0
  36. data/ext/pitchfork_http/global_variables.h +97 -0
  37. data/ext/pitchfork_http/httpdate.c +79 -0
  38. data/ext/pitchfork_http/pitchfork_http.c +4318 -0
  39. data/ext/pitchfork_http/pitchfork_http.rl +1024 -0
  40. data/ext/pitchfork_http/pitchfork_http_common.rl +76 -0
  41. data/lib/pitchfork/app/old_rails/static.rb +59 -0
  42. data/lib/pitchfork/children.rb +124 -0
  43. data/lib/pitchfork/configurator.rb +314 -0
  44. data/lib/pitchfork/const.rb +23 -0
  45. data/lib/pitchfork/http_parser.rb +206 -0
  46. data/lib/pitchfork/http_response.rb +63 -0
  47. data/lib/pitchfork/http_server.rb +822 -0
  48. data/lib/pitchfork/launcher.rb +9 -0
  49. data/lib/pitchfork/mem_info.rb +36 -0
  50. data/lib/pitchfork/message.rb +130 -0
  51. data/lib/pitchfork/mold_selector.rb +29 -0
  52. data/lib/pitchfork/preread_input.rb +33 -0
  53. data/lib/pitchfork/refork_condition.rb +21 -0
  54. data/lib/pitchfork/select_waiter.rb +9 -0
  55. data/lib/pitchfork/socket_helper.rb +199 -0
  56. data/lib/pitchfork/stream_input.rb +152 -0
  57. data/lib/pitchfork/tee_input.rb +133 -0
  58. data/lib/pitchfork/tmpio.rb +35 -0
  59. data/lib/pitchfork/version.rb +8 -0
  60. data/lib/pitchfork/worker.rb +244 -0
  61. data/lib/pitchfork.rb +158 -0
  62. data/pitchfork.gemspec +30 -0
  63. metadata +137 -0
@@ -0,0 +1,9 @@
1
+ # -*- encoding: binary -*-
2
+
3
+ # :enddoc:
4
+ $stdout.sync = $stderr.sync = true
5
+ $stdin.binmode
6
+ $stdout.binmode
7
+ $stderr.binmode
8
+
9
+ require 'pitchfork'
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pitchfork
4
+ class MemInfo
5
+ attr_reader :rss, :pss, :shared_memory
6
+
7
+ def initialize(pid)
8
+ @pid = pid
9
+ update
10
+ end
11
+
12
+ def cow_efficiency(parent_meminfo)
13
+ shared_memory.to_f / parent_meminfo.rss * 100.0
14
+ end
15
+
16
+ def update
17
+ info = parse(File.read("/proc/#{@pid}/smaps_rollup"))
18
+ @pss = info.fetch(:Pss)
19
+ @rss = info.fetch(:Rss)
20
+ @shared_memory = info.fetch(:Shared_Clean) + info.fetch(:Shared_Dirty)
21
+ self
22
+ end
23
+
24
+ private
25
+
26
+ def parse(rollup)
27
+ fields = {}
28
+ rollup.each_line do |line|
29
+ if (matchdata = line.match(/(?<field>\w+)\:\s+(?<size>\d+) kB$/))
30
+ fields[matchdata[:field].to_sym] = matchdata[:size].to_i
31
+ end
32
+ end
33
+ fields
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,130 @@
1
+ # -*- encoding: binary -*-
2
+
3
+ # :stopdoc:
4
+ module Pitchfork
5
+ class MessageSocket
6
+ unless respond_to?(:ruby2_keywords, true)
7
+ class << self
8
+ def ruby2_keywords(*args)
9
+ args
10
+ end
11
+ end
12
+ end
13
+
14
+ FD = Struct.new(:index)
15
+
16
+ def initialize(socket)
17
+ @socket = socket
18
+ end
19
+
20
+ def to_io
21
+ @socket
22
+ end
23
+
24
+ def wait(*args)
25
+ @socket.wait(*args)
26
+ end
27
+ ruby2_keywords :wait
28
+
29
+ def close_read
30
+ @socket.close_read
31
+ end
32
+
33
+ def close_write
34
+ @socket.close_write
35
+ end
36
+
37
+ def close
38
+ @socket.close
39
+ end
40
+
41
+ def sendmsg(message)
42
+ payload, ios = dump_message(message)
43
+ @socket.sendmsg(
44
+ payload,
45
+ 0,
46
+ nil,
47
+ *ios.map { |io| Socket::AncillaryData.unix_rights(io) },
48
+ )
49
+ end
50
+
51
+ def sendmsg_nonblock(message, exception: true)
52
+ payload, ios = dump_message(message)
53
+ @socket.sendmsg_nonblock(
54
+ payload,
55
+ 0,
56
+ nil,
57
+ *ios.map { |io| Socket::AncillaryData.unix_rights(io) },
58
+ exception: exception,
59
+ )
60
+ end
61
+
62
+ def recvmsg_nonblock(exception: true)
63
+ case message = @socket.recvmsg_nonblock(scm_rights: true, exception: exception)
64
+ when Array
65
+ load_message(message)
66
+ else
67
+ message
68
+ end
69
+ end
70
+
71
+ private
72
+
73
+ MARSHAL_PREFIX = (Marshal::MAJOR_VERSION.chr << Marshal::MINOR_VERSION.chr).freeze
74
+
75
+ def load_message(message)
76
+ payload, _, _, data = message
77
+
78
+ if payload.empty?
79
+ # EOF: Ruby return an empty packet on closed connection
80
+ # https://bugs.ruby-lang.org/issues/19012
81
+ return nil
82
+ end
83
+
84
+ unless payload.start_with?(MARSHAL_PREFIX)
85
+ return payload
86
+ end
87
+
88
+ klass, *args = Marshal.load(payload)
89
+ args.map! do |arg|
90
+ if arg.is_a?(FD)
91
+ data.unix_rights.fetch(arg.index)
92
+ else
93
+ arg
94
+ end
95
+ end
96
+
97
+ klass.new(*args)
98
+ end
99
+
100
+ def dump_message(message)
101
+ return [message, []] unless message.is_a?(Message)
102
+
103
+ args = message.to_a
104
+ ios = args.select { |arg| arg.is_a?(IO) || arg.is_a?(MessageSocket) }
105
+
106
+ io_index = 0
107
+ args.map! do |arg|
108
+ if arg.is_a?(IO) || arg.is_a?(MessageSocket)
109
+ fd = FD.new(io_index)
110
+ io_index += 1
111
+ fd
112
+ else
113
+ arg
114
+ end
115
+ end
116
+
117
+ [Marshal.dump([message.class, *args]), ios.map(&:to_io)]
118
+ end
119
+ end
120
+
121
+ Message = Class.new(Struct)
122
+ class Message
123
+ SpawnWorker = Message.new(:nr)
124
+ WorkerSpawned = Message.new(:nr, :pid, :generation, :pipe)
125
+ PromoteWorker = Message.new(:generation)
126
+ WorkerPromoted = Message.new(:nr, :pid, :generation)
127
+
128
+ SoftKill = Message.new(:signum)
129
+ end
130
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pitchfork
4
+ module MoldSelector
5
+ class LeastSharedMemory
6
+ def call(server)
7
+ workers = server.children.fresh_workers
8
+ if workers.empty?
9
+ server.logger.info("No current generation workers yet")
10
+ return
11
+ end
12
+ candidate = workers.shift
13
+
14
+ workers.each do |worker|
15
+ if worker.meminfo.shared_memory < candidate.meminfo.shared_memory
16
+ # We suppose that a worker with a lower amount of shared memory
17
+ # has warmed up more caches & such, hence is closer to stabilize
18
+ # making it a better candidate.
19
+ candidate = worker
20
+ end
21
+ end
22
+ parent_meminfo = server.children.mold&.meminfo || MemInfo.new(Process.pid)
23
+ cow_efficiency = candidate.meminfo.cow_efficiency(parent_meminfo)
24
+ server.logger.info("worker=#{candidate.nr} pid=#{candidate.pid} selected as new mold shared_memory_kb=#{candidate.meminfo.shared_memory} cow=#{cow_efficiency.round(1)}%")
25
+ candidate
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,33 @@
1
+ # -*- encoding: binary -*-
2
+
3
+ module Pitchfork
4
+ # This middleware is used to ensure input is buffered to memory
5
+ # or disk (depending on size) before the application is dispatched
6
+ # by entirely consuming it (from TeeInput) beforehand.
7
+ #
8
+ # Usage (in config.ru):
9
+ #
10
+ # require 'pitchfork/preread_input'
11
+ # if defined?(Pitchfork)
12
+ # use Pitchfork::PrereadInput
13
+ # end
14
+ # run YourApp.new
15
+ class PrereadInput
16
+
17
+ # :stopdoc:
18
+ def initialize(app)
19
+ @app = app
20
+ end
21
+
22
+ def call(env)
23
+ buf = ""
24
+ input = env["rack.input"]
25
+ if input.respond_to?(:rewind)
26
+ true while input.read(16384, buf)
27
+ input.rewind
28
+ end
29
+ @app.call(env)
30
+ end
31
+ # :startdoc:
32
+ end
33
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pitchfork
4
+ module ReforkCondition
5
+ class RequestsCount
6
+ def initialize(request_counts)
7
+ @limits = request_counts
8
+ end
9
+
10
+ def met?(children, logger)
11
+ if limit = @limits[children.last_generation]
12
+ if worker = children.fresh_workers.find { |w| w.requests_count >= limit }
13
+ logger.info("worker=#{worker.nr} pid=#{worker.pid} processed #{worker.requests_count} requests, triggering a refork")
14
+ return true
15
+ end
16
+ end
17
+ false
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,9 @@
1
+ module Pitchfork
2
+ # fallback for non-Linux and Linux <4.5 systems w/o EPOLLEXCLUSIVE
3
+ class SelectWaiter # :nodoc:
4
+ def get_readers(ready, readers, timeout_msec) # :nodoc:
5
+ timeout_sec = timeout_msec / 1_000.0
6
+ ret = IO.select(readers, nil, nil, timeout_sec) and ready.replace(ret[0])
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,199 @@
1
+ # -*- encoding: binary -*-
2
+ # :enddoc:
3
+ require 'socket'
4
+
5
+ module Pitchfork
6
+ module SocketHelper
7
+
8
+ # internal interface
9
+ DEFAULTS = {
10
+ # The semantics for TCP_DEFER_ACCEPT changed in Linux 2.6.32+
11
+ # with commit d1b99ba41d6c5aa1ed2fc634323449dd656899e9
12
+ # This change shouldn't affect pitchfork users behind nginx (a
13
+ # value of 1 remains an optimization).
14
+ :tcp_defer_accept => 1,
15
+
16
+ # FreeBSD, we need to override this to 'dataready' if we
17
+ # eventually support non-HTTP/1.x
18
+ :accept_filter => 'httpready',
19
+
20
+ # same default value as Mongrel
21
+ :backlog => 1024,
22
+
23
+ # favor latency over bandwidth savings
24
+ :tcp_nopush => nil,
25
+ :tcp_nodelay => true,
26
+ }
27
+
28
+ # configure platform-specific options (only tested on Linux 2.6 so far)
29
+ def accf_arg(af_name)
30
+ [ af_name, nil ].pack('a16a240')
31
+ end if RUBY_PLATFORM =~ /freebsd/ && Socket.const_defined?(:SO_ACCEPTFILTER)
32
+
33
+ def set_tcp_sockopt(sock, opt)
34
+ # just in case, even LANs can break sometimes. Linux sysadmins
35
+ # can lower net.ipv4.tcp_keepalive_* sysctl knobs to very low values.
36
+ Socket.const_defined?(:SO_KEEPALIVE) and
37
+ sock.setsockopt(:SOL_SOCKET, :SO_KEEPALIVE, 1)
38
+
39
+ if Socket.const_defined?(:TCP_NODELAY)
40
+ val = opt[:tcp_nodelay]
41
+ val = DEFAULTS[:tcp_nodelay] if val.nil?
42
+ sock.setsockopt(:IPPROTO_TCP, :TCP_NODELAY, val ? 1 : 0)
43
+ end
44
+
45
+ val = opt[:tcp_nopush]
46
+ unless val.nil?
47
+ if Socket.const_defined?(:TCP_CORK) # Linux
48
+ sock.setsockopt(:IPPROTO_TCP, :TCP_CORK, val)
49
+ elsif Socket.const_defined?(:TCP_NOPUSH) # FreeBSD
50
+ sock.setsockopt(:IPPROTO_TCP, :TCP_NOPUSH, val)
51
+ end
52
+ end
53
+
54
+ # No good reason to ever have deferred accepts off in single-threaded
55
+ # servers (except maybe benchmarking)
56
+ if Socket.const_defined?(:TCP_DEFER_ACCEPT)
57
+ # this differs from nginx, since nginx doesn't allow us to
58
+ # configure the the timeout...
59
+ seconds = opt[:tcp_defer_accept]
60
+ seconds = DEFAULTS[:tcp_defer_accept] if [true,nil].include?(seconds)
61
+ seconds = 0 unless seconds # nil/false means disable this
62
+ sock.setsockopt(:IPPROTO_TCP, :TCP_DEFER_ACCEPT, seconds)
63
+ elsif respond_to?(:accf_arg)
64
+ name = opt[:accept_filter]
65
+ name = DEFAULTS[:accept_filter] if name.nil?
66
+ sock.listen(opt[:backlog])
67
+ got = (sock.getsockopt(:SOL_SOCKET, :SO_ACCEPTFILTER) rescue nil).to_s
68
+ arg = accf_arg(name)
69
+ begin
70
+ sock.setsockopt(:SOL_SOCKET, :SO_ACCEPTFILTER, arg)
71
+ rescue => e
72
+ logger.error("#{sock_name(sock)} " \
73
+ "failed to set accept_filter=#{name} (#{e.inspect})")
74
+ logger.error("perhaps accf_http(9) needs to be loaded".freeze)
75
+ end if arg != got
76
+ end
77
+ end
78
+
79
+ def set_server_sockopt(sock, opt)
80
+ opt = DEFAULTS.merge(opt || {})
81
+
82
+ TCPSocket === sock and set_tcp_sockopt(sock, opt)
83
+
84
+ rcvbuf, sndbuf = opt.values_at(:rcvbuf, :sndbuf)
85
+ if rcvbuf || sndbuf
86
+ log_buffer_sizes(sock, "before: ")
87
+ sock.setsockopt(:SOL_SOCKET, :SO_RCVBUF, rcvbuf) if rcvbuf
88
+ sock.setsockopt(:SOL_SOCKET, :SO_SNDBUF, sndbuf) if sndbuf
89
+ log_buffer_sizes(sock, " after: ")
90
+ end
91
+ sock.listen(opt[:backlog])
92
+ rescue => e
93
+ Pitchfork.log_error(logger, "#{sock_name(sock)} #{opt.inspect}", e)
94
+ end
95
+
96
+ def log_buffer_sizes(sock, pfx = '')
97
+ rcvbuf = sock.getsockopt(:SOL_SOCKET, :SO_RCVBUF).int
98
+ sndbuf = sock.getsockopt(:SOL_SOCKET, :SO_SNDBUF).int
99
+ logger.info "#{pfx}#{sock_name(sock)} rcvbuf=#{rcvbuf} sndbuf=#{sndbuf}"
100
+ end
101
+
102
+ # creates a new server, socket. address may be a HOST:PORT or
103
+ # an absolute path to a UNIX socket. address can even be a Socket
104
+ # object in which case it is immediately returned
105
+ def bind_listen(address = '0.0.0.0:8080', opt = {})
106
+ return address unless String === address
107
+
108
+ sock = if address.start_with?('/')
109
+ if File.exist?(address)
110
+ if File.socket?(address)
111
+ begin
112
+ UNIXSocket.new(address).close
113
+ # fall through, try to bind(2) and fail with EADDRINUSE
114
+ # (or succeed from a small race condition we can't sanely avoid).
115
+ rescue Errno::ECONNREFUSED
116
+ logger.info "unlinking existing socket=#{address}"
117
+ File.unlink(address)
118
+ end
119
+ else
120
+ raise ArgumentError,
121
+ "socket=#{address} specified but it is not a socket!"
122
+ end
123
+ end
124
+ old_umask = File.umask(opt[:umask] || 0)
125
+ begin
126
+ UNIXServer.new(address)
127
+ ensure
128
+ File.umask(old_umask)
129
+ end
130
+ elsif /\A\[([a-fA-F0-9:]+)\]:(\d+)\z/ =~ address
131
+ new_tcp_server($1, $2.to_i, opt.merge(:ipv6=>true))
132
+ elsif /\A(\d+\.\d+\.\d+\.\d+):(\d+)\z/ =~ address
133
+ new_tcp_server($1, $2.to_i, opt)
134
+ else
135
+ raise ArgumentError, "Don't know how to bind: #{address}"
136
+ end
137
+ set_server_sockopt(sock, opt)
138
+ sock
139
+ end
140
+
141
+ def new_tcp_server(addr, port, opt)
142
+ # n.b. we set FD_CLOEXEC in the workers
143
+ sock = Socket.new(opt[:ipv6] ? :AF_INET6 : :AF_INET, :SOCK_STREAM)
144
+ if opt.key?(:ipv6only)
145
+ Socket.const_defined?(:IPV6_V6ONLY) or
146
+ abort "Socket::IPV6_V6ONLY not defined, upgrade Ruby and/or your OS"
147
+ sock.setsockopt(:IPPROTO_IPV6, :IPV6_V6ONLY, opt[:ipv6only] ? 1 : 0)
148
+ end
149
+ sock.setsockopt(:SOL_SOCKET, :SO_REUSEADDR, 1)
150
+ if Socket.const_defined?(:SO_REUSEPORT) && opt[:reuseport]
151
+ sock.setsockopt(:SOL_SOCKET, :SO_REUSEPORT, 1)
152
+ end
153
+ sock.bind(Socket.pack_sockaddr_in(port, addr))
154
+ sock.autoclose = false
155
+ TCPServer.for_fd(sock.fileno)
156
+ end
157
+
158
+ # returns rfc2732-style (e.g. "[::1]:666") addresses for IPv6
159
+ def tcp_name(sock)
160
+ port, addr = Socket.unpack_sockaddr_in(sock.getsockname)
161
+ addr.include?(':') ? "[#{addr}]:#{port}" : "#{addr}:#{port}"
162
+ end
163
+ module_function :tcp_name
164
+
165
+ # Returns the configuration name of a socket as a string. sock may
166
+ # be a string value, in which case it is returned as-is
167
+ # Warning: TCP sockets may not always return the name given to it.
168
+ def sock_name(sock)
169
+ case sock
170
+ when String then sock
171
+ when UNIXServer
172
+ Socket.unpack_sockaddr_un(sock.getsockname)
173
+ when TCPServer
174
+ tcp_name(sock)
175
+ when Socket
176
+ begin
177
+ tcp_name(sock)
178
+ rescue ArgumentError
179
+ Socket.unpack_sockaddr_un(sock.getsockname)
180
+ end
181
+ else
182
+ raise ArgumentError, "Unhandled class #{sock.class}: #{sock.inspect}"
183
+ end
184
+ end
185
+
186
+ module_function :sock_name
187
+
188
+ # casts a given Socket to be a TCPServer or UNIXServer
189
+ def server_cast(sock)
190
+ begin
191
+ Socket.unpack_sockaddr_in(sock.getsockname)
192
+ TCPServer.for_fd(sock.fileno)
193
+ rescue ArgumentError
194
+ UNIXServer.for_fd(sock.fileno)
195
+ end
196
+ end
197
+
198
+ end # module SocketHelper
199
+ end # module Pitchfork
@@ -0,0 +1,152 @@
1
+ # -*- encoding: binary -*-
2
+
3
+ module Pitchfork
4
+ # When processing uploads, pitchfork may expose a StreamInput object under
5
+ # "rack.input" of the Rack environment when
6
+ # Pitchfork::Configurator#rewindable_input is set to +false+
7
+ class StreamInput
8
+ # The I/O chunk size (in +bytes+) for I/O operations where
9
+ # the size cannot be user-specified when a method is called.
10
+ # The default is 16 kilobytes.
11
+ @@io_chunk_size = Pitchfork::Const::CHUNK_SIZE # :nodoc:
12
+
13
+ # Initializes a new StreamInput object. You normally do not have to call
14
+ # this unless you are writing an HTTP server.
15
+ def initialize(socket, request) # :nodoc:
16
+ @chunked = request.content_length.nil?
17
+ @socket = socket
18
+ @parser = request
19
+ @buf = request.buf
20
+ @rbuf = ''
21
+ @bytes_read = 0
22
+ filter_body(@rbuf, @buf) unless @buf.empty?
23
+ end
24
+
25
+ # :call-seq:
26
+ # ios.read([length [, buffer ]]) => string, buffer, or nil
27
+ #
28
+ # Reads at most length bytes from the I/O stream, or to the end of
29
+ # file if length is omitted or is nil. length must be a non-negative
30
+ # integer or nil. If the optional buffer argument is present, it
31
+ # must reference a String, which will receive the data.
32
+ #
33
+ # At end of file, it returns nil or '' depend on length.
34
+ # ios.read() and ios.read(nil) returns ''.
35
+ # ios.read(length [, buffer]) returns nil.
36
+ #
37
+ # If the Content-Length of the HTTP request is known (as is the common
38
+ # case for POST requests), then ios.read(length [, buffer]) will block
39
+ # until the specified length is read (or it is the last chunk).
40
+ # Otherwise, for uncommon "Transfer-Encoding: chunked" requests,
41
+ # ios.read(length [, buffer]) will return immediately if there is
42
+ # any data and only block when nothing is available (providing
43
+ # IO#readpartial semantics).
44
+ def read(length = nil, rv = '')
45
+ if length
46
+ if length <= @rbuf.size
47
+ length < 0 and raise ArgumentError, "negative length #{length} given"
48
+ rv.replace(@rbuf.slice!(0, length))
49
+ else
50
+ to_read = length - @rbuf.size
51
+ rv.replace(@rbuf.slice!(0, @rbuf.size))
52
+ until to_read == 0 || eof? || (rv.size > 0 && @chunked)
53
+ begin
54
+ @socket.readpartial(to_read, @buf)
55
+ rescue EOFError
56
+ eof!
57
+ end
58
+ filter_body(@rbuf, @buf)
59
+ rv << @rbuf
60
+ to_read -= @rbuf.size
61
+ end
62
+ @rbuf.clear
63
+ end
64
+ rv = nil if rv.empty? && length != 0
65
+ else
66
+ read_all(rv)
67
+ end
68
+ rv
69
+ end
70
+
71
+ # :call-seq:
72
+ # ios.gets => string or nil
73
+ #
74
+ # Reads the next ``line'' from the I/O stream; lines are separated
75
+ # by the global record separator ($/, typically "\n"). A global
76
+ # record separator of nil reads the entire unread contents of ios.
77
+ # Returns nil if called at the end of file.
78
+ # This takes zero arguments for strict Rack::Lint compatibility,
79
+ # unlike IO#gets.
80
+ def gets(sep = $/)
81
+ if sep.nil?
82
+ read_all(rv = '')
83
+ return rv.empty? ? nil : rv
84
+ end
85
+ re = /\A(.*?#{Regexp.escape(sep)})/
86
+
87
+ begin
88
+ @rbuf.sub!(re, '') and return $1
89
+ return @rbuf.empty? ? nil : @rbuf.slice!(0, @rbuf.size) if eof?
90
+ @socket.readpartial(@@io_chunk_size, @buf) or eof!
91
+ filter_body(once = '', @buf)
92
+ @rbuf << once
93
+ end while true
94
+ end
95
+
96
+ # :call-seq:
97
+ # ios.each { |line| block } => ios
98
+ #
99
+ # Executes the block for every ``line'' in *ios*, where lines are
100
+ # separated by the global record separator ($/, typically "\n").
101
+ def each
102
+ while line = gets
103
+ yield line
104
+ end
105
+
106
+ self # Rack does not specify what the return value is here
107
+ end
108
+
109
+ private
110
+
111
+ def eof?
112
+ if @parser.body_eof?
113
+ while @chunked && ! @parser.parse
114
+ once = @socket.readpartial(@@io_chunk_size) or eof!
115
+ @buf << once
116
+ end
117
+ @socket = nil
118
+ true
119
+ else
120
+ false
121
+ end
122
+ end
123
+
124
+ def filter_body(dst, src)
125
+ rv = @parser.filter_body(dst, src)
126
+ @bytes_read += dst.size
127
+ rv
128
+ end
129
+
130
+ def read_all(dst)
131
+ dst.replace(@rbuf)
132
+ @socket or return
133
+ until eof?
134
+ @socket.readpartial(@@io_chunk_size, @buf) or eof!
135
+ filter_body(@rbuf, @buf)
136
+ dst << @rbuf
137
+ end
138
+ ensure
139
+ @rbuf.clear
140
+ end
141
+
142
+ def eof!
143
+ # in case client only did a premature shutdown(SHUT_WR)
144
+ # we do support clients that shutdown(SHUT_WR) after the
145
+ # _entire_ request has been sent, and those will not have
146
+ # raised EOFError on us.
147
+ @socket.shutdown if @socket
148
+ ensure
149
+ raise Pitchfork::ClientShutdown, "bytes_read=#{@bytes_read}", []
150
+ end
151
+ end
152
+ end