rainbows 2.1.0 → 3.0.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.
Files changed (71) hide show
  1. data/GIT-VERSION-GEN +1 -1
  2. data/GNUmakefile +2 -3
  3. data/Rakefile +5 -2
  4. data/lib/rainbows.rb +72 -54
  5. data/lib/rainbows/base.rb +7 -9
  6. data/lib/rainbows/client.rb +25 -4
  7. data/lib/rainbows/const.rb +1 -1
  8. data/lib/rainbows/coolio.rb +6 -3
  9. data/lib/rainbows/coolio/client.rb +78 -57
  10. data/lib/rainbows/coolio/core.rb +1 -4
  11. data/lib/rainbows/coolio/heartbeat.rb +2 -3
  12. data/lib/rainbows/coolio/master.rb +3 -2
  13. data/lib/rainbows/coolio/{deferred_chunk_response.rb → response_chunk_pipe.rb} +1 -2
  14. data/lib/rainbows/coolio/{deferred_response.rb → response_pipe.rb} +1 -1
  15. data/lib/rainbows/coolio/thread_client.rb +4 -6
  16. data/lib/rainbows/coolio_fiber_spawn.rb +1 -1
  17. data/lib/rainbows/coolio_thread_pool.rb +1 -1
  18. data/lib/rainbows/coolio_thread_pool/watcher.rb +2 -4
  19. data/lib/rainbows/coolio_thread_spawn.rb +1 -1
  20. data/lib/rainbows/dev_fd_response.rb +2 -2
  21. data/lib/rainbows/error.rb +5 -7
  22. data/lib/rainbows/ev_core.rb +20 -7
  23. data/lib/rainbows/event_machine.rb +6 -5
  24. data/lib/rainbows/event_machine/client.rb +46 -53
  25. data/lib/rainbows/event_machine/response_pipe.rb +2 -3
  26. data/lib/rainbows/fiber/base.rb +5 -5
  27. data/lib/rainbows/fiber/body.rb +4 -13
  28. data/lib/rainbows/fiber/coolio/heartbeat.rb +1 -3
  29. data/lib/rainbows/fiber/coolio/server.rb +4 -7
  30. data/lib/rainbows/fiber_pool.rb +1 -1
  31. data/lib/rainbows/fiber_spawn.rb +2 -2
  32. data/lib/rainbows/http_parser.rb +12 -0
  33. data/lib/rainbows/http_server.rb +5 -7
  34. data/lib/rainbows/max_body.rb +2 -2
  35. data/lib/rainbows/never_block/core.rb +1 -1
  36. data/lib/rainbows/process_client.rb +15 -29
  37. data/lib/rainbows/queue_pool.rb +1 -3
  38. data/lib/rainbows/rack_input.rb +3 -3
  39. data/lib/rainbows/response.rb +164 -38
  40. data/lib/rainbows/revactor.rb +5 -65
  41. data/lib/rainbows/revactor/client.rb +60 -0
  42. data/lib/rainbows/revactor/{body.rb → client/methods.rb} +14 -14
  43. data/lib/rainbows/revactor/{tee_socket.rb → client/tee_socket.rb} +1 -1
  44. data/lib/rainbows/sendfile.rb +1 -2
  45. data/lib/rainbows/server_token.rb +1 -1
  46. data/lib/rainbows/thread_pool.rb +9 -9
  47. data/lib/rainbows/thread_spawn.rb +7 -6
  48. data/lib/rainbows/thread_timeout.rb +1 -1
  49. data/lib/rainbows/writer_thread_pool.rb +9 -25
  50. data/lib/rainbows/writer_thread_pool/client.rb +44 -1
  51. data/lib/rainbows/writer_thread_spawn.rb +2 -11
  52. data/lib/rainbows/writer_thread_spawn/client.rb +53 -13
  53. data/rainbows.gemspec +3 -12
  54. data/t/async_chunk_app.ru +62 -0
  55. data/t/byte-range-common.sh +142 -0
  56. data/t/t0022-copy_stream-byte-range.sh +2 -111
  57. data/t/t0023-sendfile-byte-range.sh +2 -32
  58. data/t/t0025-write-on-close.sh +23 -0
  59. data/t/t0040-keepalive_requests-setting.sh +0 -5
  60. data/t/t0402-async-keepalive.sh +146 -0
  61. data/t/t0500-cramp-streaming.sh +2 -0
  62. data/t/t0501-cramp-rainsocket.sh +2 -0
  63. data/t/t9000-rack-app-pool.sh +1 -1
  64. data/t/test_isolate.rb +5 -10
  65. data/t/test_isolate_cramp.rb +26 -0
  66. data/t/write-on-close.ru +11 -0
  67. metadata +33 -30
  68. data/lib/rainbows/coolio/sendfile.rb +0 -17
  69. data/lib/rainbows/response/body.rb +0 -127
  70. data/lib/rainbows/response/range.rb +0 -34
  71. data/lib/rainbows/timed_read.rb +0 -28
@@ -5,8 +5,8 @@ module Rainbows::EventMachine::ResponsePipe
5
5
  # so a single buffer for all clients will work safely
6
6
  BUF = ''
7
7
 
8
- def initialize(client, alive, body)
9
- @client, @alive, @body = client, alive, body
8
+ def initialize(client)
9
+ @client = client
10
10
  end
11
11
 
12
12
  def notify_readable
@@ -22,7 +22,6 @@ module Rainbows::EventMachine::ResponsePipe
22
22
  end
23
23
 
24
24
  def unbind
25
- @body.close if @body.respond_to?(:close)
26
25
  @client.next!
27
26
  @io.close unless @io.closed?
28
27
  end
@@ -19,7 +19,7 @@ module Rainbows::Fiber::Base
19
19
  # will cause it.
20
20
  def schedule(&block)
21
21
  begin
22
- G.tick
22
+ Rainbows.tick
23
23
  t = schedule_sleepers
24
24
  ret = select(RD.compact.concat(LISTENERS), WR.compact, nil, t)
25
25
  rescue Errno::EINTR
@@ -56,16 +56,16 @@ module Rainbows::Fiber::Base
56
56
  end
57
57
 
58
58
  def process(client)
59
- G.cur += 1
60
- process_client(client)
59
+ Rainbows.cur += 1
60
+ client.process_loop
61
61
  ensure
62
- G.cur -= 1
62
+ Rainbows.cur -= 1
63
63
  ZZ.delete(client.f)
64
64
  end
65
65
 
66
66
  def self.setup(klass, app)
67
67
  require 'rainbows/fiber/body'
68
- klass.__send__(:include, Rainbows::Fiber::Body)
68
+ Rainbows::Client.__send__(:include, Rainbows::Fiber::Body)
69
69
  self.const_set(:APP, app)
70
70
  end
71
71
  end
@@ -5,20 +5,15 @@
5
5
  # this is meant to be included _after_ Rainbows::Response::Body
6
6
  module Rainbows::Fiber::Body # :nodoc:
7
7
 
8
- # TODO non-blocking splice(2) under Linux
9
- ALIASES = {
10
- :write_body_stream => :write_body_each
11
- }
12
-
13
8
  # the sendfile 1.0.0+ gem includes IO#sendfile_nonblock
14
9
  if IO.method_defined?(:sendfile_nonblock)
15
- def write_body_file(client, body, range)
16
- sock, n, body = client.to_io, nil, body_to_io(body)
10
+ def write_body_file(body, range)
11
+ sock, n, body = to_io, nil, body_to_io(body)
17
12
  offset, count = range ? range : [ 0, body.stat.size ]
18
13
  begin
19
14
  offset += (n = sock.sendfile_nonblock(body, offset, count))
20
15
  rescue Errno::EAGAIN
21
- client.kgio_wait_writable
16
+ kgio_wait_writable
22
17
  retry
23
18
  rescue EOFError
24
19
  break
@@ -26,13 +21,9 @@ module Rainbows::Fiber::Body # :nodoc:
26
21
  ensure
27
22
  close_if_private(body)
28
23
  end
29
- else
30
- ALIASES[:write_body] = :write_body_each
31
24
  end
32
25
 
33
26
  def self.included(klass)
34
- ALIASES.each do |new_method, orig_method|
35
- klass.__send__(:alias_method, new_method, orig_method)
36
- end
27
+ klass.__send__ :alias_method, :write_body_stream, :write_body_each
37
28
  end
38
29
  end
@@ -1,12 +1,10 @@
1
1
  # -*- encoding: binary -*-
2
2
  # :enddoc:
3
3
  class Rainbows::Fiber::Coolio::Heartbeat < Coolio::TimerWatcher
4
- G = Rainbows::G
5
-
6
4
  # ZZ gets populated by read_expire in rainbows/fiber/io/methods
7
5
  ZZ = Rainbows::Fiber::ZZ
8
6
  def on_timer
9
- exit if (! G.tick && G.cur <= 0)
7
+ exit if (! Rainbows.tick && Rainbows.cur <= 0)
10
8
  now = Time.now
11
9
  fibs = []
12
10
  ZZ.delete_if { |fib, time| now >= time ? fibs << fib : ! fib.alive? }
@@ -1,9 +1,6 @@
1
1
  # -*- encoding: binary -*-
2
2
  # :enddoc:
3
3
  class Rainbows::Fiber::Coolio::Server < Coolio::IOWatcher
4
- G = Rainbows::G
5
- include Rainbows::ProcessClient
6
-
7
4
  def to_io
8
5
  @io
9
6
  end
@@ -19,14 +16,14 @@ class Rainbows::Fiber::Coolio::Server < Coolio::IOWatcher
19
16
  end
20
17
 
21
18
  def on_readable
22
- return if G.cur >= MAX
19
+ return if Rainbows.cur >= MAX
23
20
  c = @io.kgio_tryaccept and Fiber.new { process(c) }.resume
24
21
  end
25
22
 
26
23
  def process(io)
27
- G.cur += 1
28
- process_client(io)
24
+ Rainbows.cur += 1
25
+ io.process_loop
29
26
  ensure
30
- G.cur -= 1
27
+ Rainbows.cur -= 1
31
28
  end
32
29
  end
@@ -34,6 +34,6 @@ module Rainbows::FiberPool
34
34
  end
35
35
  rescue => e
36
36
  Rainbows::Error.listen_loop(e)
37
- end while G.alive || G.cur > 0
37
+ end while Rainbows.cur_alive
38
38
  end
39
39
  end
@@ -17,12 +17,12 @@ module Rainbows::FiberSpawn
17
17
 
18
18
  begin
19
19
  schedule do |l|
20
- break if G.cur >= limit
20
+ break if Rainbows.cur >= limit
21
21
  io = l.kgio_tryaccept or next
22
22
  Fiber.new { process(io) }.resume
23
23
  end
24
24
  rescue => e
25
25
  Rainbows::Error.listen_loop(e)
26
- end while G.alive || G.cur > 0
26
+ end while Rainbows.cur_alive
27
27
  end
28
28
  end
@@ -0,0 +1,12 @@
1
+ # -*- encoding: binary -*-
2
+ # :enddoc:
3
+ # avoid modifying Unicorn::HttpParser
4
+ class Rainbows::HttpParser < Unicorn::HttpParser
5
+ def self.quit
6
+ alias_method :next?, :never!
7
+ end
8
+
9
+ def never!
10
+ false
11
+ end
12
+ end
@@ -2,14 +2,12 @@
2
2
  # :enddoc:
3
3
 
4
4
  class Rainbows::HttpServer < Unicorn::HttpServer
5
- G = Rainbows::G
6
-
7
5
  def self.setup(block)
8
- G.server.instance_eval(&block)
6
+ Rainbows.server.instance_eval(&block)
9
7
  end
10
8
 
11
9
  def initialize(app, options)
12
- G.server = self
10
+ Rainbows.server = self
13
11
  @logger = Unicorn::Configurator::DEFAULTS[:logger]
14
12
  rv = super(app, options)
15
13
  defined?(@use) or use(:Base)
@@ -21,7 +19,7 @@ class Rainbows::HttpServer < Unicorn::HttpServer
21
19
  Unicorn::Util.reopen_logs
22
20
  logger.info "worker=#{worker_nr} done reopening logs"
23
21
  rescue
24
- G.quit! # let the master reopen and refork us
22
+ Rainbows.quit! # let the master reopen and refork us
25
23
  end
26
24
 
27
25
  # Add one second to the timeout since our fchmod heartbeat is less
@@ -36,7 +34,7 @@ class Rainbows::HttpServer < Unicorn::HttpServer
36
34
 
37
35
  def load_config!
38
36
  use :Base
39
- G.kato = 5
37
+ Rainbows.keepalive_timeout = 5
40
38
  Rainbows.max_bytes = 1024 * 1024
41
39
  @worker_connections = nil
42
40
  super
@@ -91,7 +89,7 @@ class Rainbows::HttpServer < Unicorn::HttpServer
91
89
  def keepalive_timeout(nr)
92
90
  (Integer === nr && nr >= 0) or
93
91
  raise ArgumentError, "keepalive_timeout must be a non-negative Integer"
94
- G.kato = nr
92
+ Rainbows.keepalive_timeout = nr
95
93
  end
96
94
 
97
95
  def keepalive_requests(nr)
@@ -52,7 +52,7 @@ class Rainbows::MaxBody
52
52
  # if it's reconfigured
53
53
  def self.setup # :nodoc:
54
54
  Rainbows.max_bytes or return
55
- case Rainbows::G.server.use
55
+ case Rainbows.server.use
56
56
  when :Rev, :Coolio, :EventMachine, :NeverBlock,
57
57
  :RevThreadSpawn, :RevThreadPool,
58
58
  :CoolioThreadSpawn, :CoolioThreadPool
@@ -60,7 +60,7 @@ class Rainbows::MaxBody
60
60
  end
61
61
 
62
62
  # force ourselves to the outermost middleware layer
63
- Rainbows::G.server.app = self.new(Rainbows::G.server.app)
63
+ Rainbows.server.app = self.new(Rainbows.server.app)
64
64
  end
65
65
 
66
66
  # Rack response returned when there's an error
@@ -8,7 +8,7 @@ module Rainbows::NeverBlock::Core
8
8
  base = o[:backend].to_s.gsub!(/([a-z])([A-Z])/, '\1_\2').downcase!
9
9
  require "rainbows/never_block/#{base}"
10
10
  client_class = Rainbows::NeverBlock::Client
11
- client_class.superclass.const_set(:APP, Rainbows::G.server.app)
11
+ client_class.superclass.const_set(:APP, Rainbows.server.app)
12
12
  client_class.const_set(:POOL, pool)
13
13
  logger.info "NeverBlock/#{o[:backend]} pool_size=#{o[:pool_size]}"
14
14
  end
@@ -1,55 +1,41 @@
1
1
  # -*- encoding: binary -*-
2
- # :enddoc:
3
- require 'rainbows/rack_input'
4
2
  module Rainbows::ProcessClient
5
- G = Rainbows::G
6
3
  include Rainbows::Response
7
- HttpParser = Unicorn::HttpParser
8
4
  include Rainbows::RackInput
9
5
  include Rainbows::Const
10
6
 
11
- # once a client is accepted, it is processed in its entirety here
12
- # in 3 easy steps: read request, call app, write app response
13
- # this is used by synchronous concurrency models
14
- # Base, ThreadSpawn, ThreadPool
15
- def process_client(client) # :nodoc:
16
- hp = HttpParser.new
17
- client.kgio_read!(16384, buf = hp.buf)
18
- remote_addr = client.kgio_addr
19
- alive = false
7
+ def process_loop
8
+ @hp = hp = Rainbows::HttpParser.new
9
+ kgio_read!(16384, buf = hp.buf) or return
20
10
 
21
11
  begin # loop
22
12
  until env = hp.parse
23
- client.timed_read(buf2 ||= "") or return
13
+ timed_read(buf2 ||= "") or return
24
14
  buf << buf2
25
15
  end
26
16
 
27
- set_input(env, hp, client)
28
- env[REMOTE_ADDR] = remote_addr
29
- status, headers, body = APP.call(env.update(RACK_DEFAULTS))
17
+ set_input(env, hp)
18
+ env[REMOTE_ADDR] = kgio_addr
19
+ status, headers, body = APP.call(env.merge!(RACK_DEFAULTS))
30
20
 
31
21
  if 100 == status.to_i
32
- client.write(EXPECT_100_RESPONSE)
22
+ write(EXPECT_100_RESPONSE)
33
23
  env.delete(HTTP_EXPECT)
34
24
  status, headers, body = APP.call(env)
35
25
  end
36
-
37
- if hp.headers?
38
- headers = HH.new(headers)
39
- range = make_range!(env, status, headers) and status = range.shift
40
- alive = hp.next? && G.alive
41
- headers[CONNECTION] = alive ? KEEP_ALIVE : CLOSE
42
- client.write(response_header(status, headers))
43
- end
44
- write_body(client, body, range)
26
+ write_response(status, headers, body, alive = @hp.next?)
45
27
  end while alive
46
28
  # if we get any error, try to write something back to the client
47
29
  # assuming we haven't closed the socket, but don't get hung up
48
30
  # if the socket is already closed or broken. We'll always ensure
49
31
  # the socket is closed at the end of this function
50
32
  rescue => e
51
- Rainbows::Error.write(client, e)
33
+ handle_error(e)
52
34
  ensure
53
- client.close unless client.closed?
35
+ close unless closed?
36
+ end
37
+
38
+ def handle_error(e)
39
+ Rainbows::Error.write(self, e)
54
40
  end
55
41
  end
@@ -6,8 +6,6 @@ require 'thread'
6
6
  # This is NOT used for the ThreadPool class, since that class does not
7
7
  # need a userspace Queue.
8
8
  class Rainbows::QueuePool < Struct.new(:queue, :threads)
9
- G = Rainbows::G
10
-
11
9
  def initialize(size = 20, &block)
12
10
  q = Queue.new
13
11
  self.threads = (1..size).map do
@@ -23,7 +21,7 @@ class Rainbows::QueuePool < Struct.new(:queue, :threads)
23
21
  def quit!
24
22
  threads.each { |_| queue << nil }
25
23
  threads.delete_if do |t|
26
- G.tick
24
+ Rainbows.tick
27
25
  t.alive? ? t.join(0.01) : true
28
26
  end until threads.empty?
29
27
  end
@@ -10,8 +10,8 @@ module Rainbows::RackInput
10
10
  const_set(:IC, Unicorn::HttpRequest.input_class)
11
11
  end
12
12
 
13
- def set_input(env, hp, client)
14
- env[RACK_INPUT] = 0 == hp.content_length ? NULL_IO : IC.new(client, hp)
15
- env[CLIENT_IO] = client
13
+ def set_input(env, hp)
14
+ env[RACK_INPUT] = 0 == hp.content_length ? NULL_IO : IC.new(self, hp)
15
+ env[CLIENT_IO] = self
16
16
  end
17
17
  end
@@ -1,59 +1,185 @@
1
1
  # -*- encoding: binary -*-
2
2
  # :enddoc:
3
- require 'time' # for Time#httpdate
4
-
5
3
  module Rainbows::Response
6
- autoload :Body, 'rainbows/response/body'
7
- autoload :Range, 'rainbows/response/range'
4
+ include Unicorn::HttpResponse
5
+ Close = "close"
6
+ KeepAlive = "keep-alive"
7
+ Content_Length = "Content-Length".freeze
8
+ Transfer_Encoding = "Transfer-Encoding".freeze
8
9
 
9
- CODES = Unicorn::HttpResponse::CODES
10
- CRLF = "\r\n"
10
+ # private file class for IO objects opened by Rainbows! itself (and not
11
+ # the app or middleware)
12
+ class F < File; end
11
13
 
12
- # freeze headers we may set as hash keys for a small speedup
13
- CONNECTION = "Connection".freeze
14
- CLOSE = "close"
15
- KEEP_ALIVE = "keep-alive"
16
- HH = Rack::Utils::HeaderHash
14
+ # called after forking
15
+ def self.setup(klass)
16
+ Kgio.accept_class = Rainbows::Client
17
+ 0 == Rainbows.keepalive_timeout and
18
+ Rainbows::HttpParser.keepalive_requests = 0
19
+ end
17
20
 
18
- def response_header(status, headers)
21
+ def write_headers(status, headers, alive)
22
+ @hp.headers? or return
19
23
  status = CODES[status.to_i] || status
20
- rv = "HTTP/1.1 #{status}\r\n" \
21
- "Date: #{Time.now.httpdate}\r\n" \
22
- "Status: #{status}\r\n"
24
+ buf = "HTTP/1.1 #{status}\r\n" \
25
+ "Date: #{httpdate}\r\n" \
26
+ "Status: #{status}\r\n" \
27
+ "Connection: #{alive ? KeepAlive : Close}\r\n"
23
28
  headers.each do |key, value|
24
- next if %r{\A(?:X-Rainbows-|Date\z|Status\z)}i =~ key
29
+ next if %r{\A(?:X-Rainbows-|Date\z|Connection\z)}i =~ key
25
30
  if value =~ /\n/
26
31
  # avoiding blank, key-only cookies with /\n+/
27
- rv << value.split(/\n+/).map! { |v| "#{key}: #{v}\r\n" }.join('')
32
+ buf << value.split(/\n+/).map! { |v| "#{key}: #{v}\r\n" }.join
28
33
  else
29
- rv << "#{key}: #{value}\r\n"
34
+ buf << "#{key}: #{value}\r\n"
30
35
  end
31
36
  end
32
- rv << CRLF
37
+ write(buf << CRLF)
33
38
  end
34
39
 
35
- # called after forking
36
- def self.setup(klass)
37
- Rainbows::G.kato == 0 and KEEP_ALIVE.replace(CLOSE)
38
- range_class = body_class = klass
39
- case Rainbows::Const::RACK_DEFAULTS['rainbows.model']
40
- when :WriterThreadSpawn
41
- body_class = Rainbows::WriterThreadSpawn::Client
42
- range_class = Rainbows::HttpServer
43
- when :EventMachine, :NeverBlock
44
- range_class = nil # :<
45
- end
46
- return if body_class.included_modules.include?(Body)
47
- body_class.__send__(:include, Body)
48
- sf = IO.respond_to?(:copy_stream) || IO.method_defined?(:sendfile_nonblock)
49
- if range_class
50
- range_class.__send__(:include, sf ? Range : NoRange)
40
+ def close_if_private(io)
41
+ io.close if F === io
42
+ end
43
+
44
+ def io_for_fd(fd)
45
+ Rainbows::FD_MAP.delete(fd) || F.for_fd(fd)
46
+ end
47
+
48
+ # to_io is not part of the Rack spec, but make an exception here
49
+ # since we can conserve path lookups and file descriptors.
50
+ # \Rainbows! will never get here without checking for the existence
51
+ # of body.to_path first.
52
+ def body_to_io(body)
53
+ if body.respond_to?(:to_io)
54
+ body.to_io
55
+ else
56
+ # try to take advantage of Rainbows::DevFdResponse, calling F.open
57
+ # is a last resort
58
+ path = body.to_path
59
+ %r{\A/dev/fd/(\d+)\z} =~ path ? io_for_fd($1.to_i) : F.open(path)
51
60
  end
52
61
  end
53
62
 
54
- module NoRange
55
- # dummy method if we can't send range responses
56
- def make_range!(env, status, headers)
63
+ module Each
64
+ # generic body writer, used for most dynamically-generated responses
65
+ def write_body_each(body)
66
+ body.each { |chunk| write(chunk) }
67
+ end
68
+
69
+ # generic response writer, used for most dynamically-generated responses
70
+ # and also when IO.copy_stream and/or IO#sendfile_nonblock is unavailable
71
+ def write_response(status, headers, body, alive)
72
+ write_headers(status, headers, alive)
73
+ write_body_each(body)
74
+ ensure
75
+ body.close if body.respond_to?(:close)
57
76
  end
58
77
  end
78
+ include Each
79
+
80
+ if IO.method_defined?(:sendfile_nonblock)
81
+ module Sendfile
82
+ def write_body_file(body, range)
83
+ io = body_to_io(body)
84
+ range ? sendfile(io, range[0], range[1]) : sendfile(io, 0)
85
+ ensure
86
+ close_if_private(io)
87
+ end
88
+ end
89
+ include Sendfile
90
+ end
91
+
92
+ if IO.respond_to?(:copy_stream)
93
+ unless IO.method_defined?(:sendfile_nonblock)
94
+ module CopyStream
95
+ def write_body_file(body, range)
96
+ range ? IO.copy_stream(body, self, range[1], range[0]) :
97
+ IO.copy_stream(body, self, nil, 0)
98
+ end
99
+ end
100
+ include CopyStream
101
+ end
102
+
103
+ # write_body_stream is an alias for write_body_each if IO.copy_stream
104
+ # isn't used or available.
105
+ def write_body_stream(body)
106
+ IO.copy_stream(io = body_to_io(body), self)
107
+ ensure
108
+ close_if_private(io)
109
+ end
110
+ else # ! IO.respond_to?(:copy_stream)
111
+ alias write_body_stream write_body_each
112
+ end # ! IO.respond_to?(:copy_stream)
113
+
114
+ if IO.method_defined?(:sendfile_nonblock) || IO.respond_to?(:copy_stream)
115
+ HTTP_RANGE = 'HTTP_RANGE'
116
+ Content_Range = 'Content-Range'.freeze
117
+
118
+ # This does not support multipart responses (does anybody actually
119
+ # use those?)
120
+ def sendfile_range(status, headers)
121
+ 200 == status.to_i &&
122
+ /\Abytes=(\d+-\d*|\d*-\d+)\z/ =~ @hp.env[HTTP_RANGE] or
123
+ return
124
+ a, b = $1.split(/-/)
125
+
126
+ # HeaderHash is quite expensive, and Rack::File currently
127
+ # uses a regular Ruby Hash with properly-cased headers the
128
+ # same way they're presented in rfc2616.
129
+ headers = Rack::Utils::HeaderHash.new(headers) unless Hash === headers
130
+ clen = headers[Content_Length] or return
131
+ size = clen.to_i
132
+
133
+ if b.nil? # bytes=M-
134
+ offset = a.to_i
135
+ count = size - offset
136
+ elsif a.empty? # bytes=-N
137
+ offset = size - b.to_i
138
+ count = size - offset
139
+ else # bytes=M-N
140
+ offset = a.to_i
141
+ count = b.to_i + 1 - offset
142
+ end
143
+
144
+ if 0 > count || offset >= size
145
+ headers[Content_Length] = "0"
146
+ headers[Content_Range] = "bytes */#{clen}"
147
+ return 416, headers, nil
148
+ else
149
+ count = size if count > size
150
+ headers[Content_Length] = count.to_s
151
+ headers[Content_Range] = "bytes #{offset}-#{offset+count-1}/#{clen}"
152
+ return 206, headers, [ offset, count ]
153
+ end
154
+ end
155
+
156
+ def write_response_path(status, headers, body, alive)
157
+ if File.file?(body.to_path)
158
+ if r = sendfile_range(status, headers)
159
+ status, headers, range = r
160
+ write_headers(status, headers, alive)
161
+ write_body_file(body, range) if range
162
+ else
163
+ write_headers(status, headers, alive)
164
+ write_body_file(body, nil)
165
+ end
166
+ else
167
+ write_headers(status, headers, alive)
168
+ write_body_stream(body)
169
+ end
170
+ ensure
171
+ body.close if body.respond_to?(:close)
172
+ end
173
+
174
+ module ToPath
175
+ def write_response(status, headers, body, alive)
176
+ if body.respond_to?(:to_path)
177
+ write_response_path(status, headers, body, alive)
178
+ else
179
+ super
180
+ end
181
+ end
182
+ end
183
+ include ToPath
184
+ end # IO.respond_to?(:copy_stream) || IO.method_defined?(:sendfile_nonblock)
59
185
  end