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.
- checksums.yaml +7 -0
- data/.git-blame-ignore-revs +3 -0
- data/.gitattributes +5 -0
- data/.github/workflows/ci.yml +30 -0
- data/.gitignore +23 -0
- data/COPYING +674 -0
- data/Dockerfile +4 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +30 -0
- data/LICENSE +67 -0
- data/README.md +123 -0
- data/Rakefile +72 -0
- data/docs/Application_Timeouts.md +74 -0
- data/docs/CONFIGURATION.md +388 -0
- data/docs/DESIGN.md +86 -0
- data/docs/FORK_SAFETY.md +80 -0
- data/docs/PHILOSOPHY.md +90 -0
- data/docs/REFORKING.md +113 -0
- data/docs/SIGNALS.md +38 -0
- data/docs/TUNING.md +106 -0
- data/examples/constant_caches.ru +43 -0
- data/examples/echo.ru +25 -0
- data/examples/hello.ru +5 -0
- data/examples/nginx.conf +156 -0
- data/examples/pitchfork.conf.minimal.rb +5 -0
- data/examples/pitchfork.conf.rb +77 -0
- data/examples/unicorn.socket +11 -0
- data/exe/pitchfork +116 -0
- data/ext/pitchfork_http/CFLAGS +13 -0
- data/ext/pitchfork_http/c_util.h +116 -0
- data/ext/pitchfork_http/child_subreaper.h +25 -0
- data/ext/pitchfork_http/common_field_optimization.h +130 -0
- data/ext/pitchfork_http/epollexclusive.h +124 -0
- data/ext/pitchfork_http/ext_help.h +38 -0
- data/ext/pitchfork_http/extconf.rb +14 -0
- data/ext/pitchfork_http/global_variables.h +97 -0
- data/ext/pitchfork_http/httpdate.c +79 -0
- data/ext/pitchfork_http/pitchfork_http.c +4318 -0
- data/ext/pitchfork_http/pitchfork_http.rl +1024 -0
- data/ext/pitchfork_http/pitchfork_http_common.rl +76 -0
- data/lib/pitchfork/app/old_rails/static.rb +59 -0
- data/lib/pitchfork/children.rb +124 -0
- data/lib/pitchfork/configurator.rb +314 -0
- data/lib/pitchfork/const.rb +23 -0
- data/lib/pitchfork/http_parser.rb +206 -0
- data/lib/pitchfork/http_response.rb +63 -0
- data/lib/pitchfork/http_server.rb +822 -0
- data/lib/pitchfork/launcher.rb +9 -0
- data/lib/pitchfork/mem_info.rb +36 -0
- data/lib/pitchfork/message.rb +130 -0
- data/lib/pitchfork/mold_selector.rb +29 -0
- data/lib/pitchfork/preread_input.rb +33 -0
- data/lib/pitchfork/refork_condition.rb +21 -0
- data/lib/pitchfork/select_waiter.rb +9 -0
- data/lib/pitchfork/socket_helper.rb +199 -0
- data/lib/pitchfork/stream_input.rb +152 -0
- data/lib/pitchfork/tee_input.rb +133 -0
- data/lib/pitchfork/tmpio.rb +35 -0
- data/lib/pitchfork/version.rb +8 -0
- data/lib/pitchfork/worker.rb +244 -0
- data/lib/pitchfork.rb +158 -0
- data/pitchfork.gemspec +30 -0
- metadata +137 -0
@@ -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
|