tests: "wc -c" portability for *BSDs
[rainbows.git] / lib / rainbows / dev_fd_response.rb
blob7baccfc901210f8d9905676971323c20b7d95889
1 # -*- encoding: binary -*-
3 # Rack response middleware wrapping any IO-like object with an
4 # OS-level file descriptor associated with it.  May also be used to
5 # create responses from integer file descriptors or existing +IO+
6 # objects.  This may be used in conjunction with the #to_path method
7 # on servers that support it to pass arbitrary file descriptors into
8 # the HTTP response without additional open(2) syscalls
10 class Rainbows::DevFdResponse < Struct.new(:app)
12   # :stopdoc:
13   FD_MAP = Rainbows::FD_MAP
14   Content_Length = "Content-Length".freeze
15   Transfer_Encoding = "Transfer-Encoding".freeze
16   Rainbows_autochunk = "rainbows.autochunk".freeze
17   Rainbows_model = "rainbows.model"
18   HTTP_VERSION = "HTTP_VERSION"
19   Chunked = "chunked"
20   include Rack::Utils
22   # Rack middleware entry point, we'll just pass through responses
23   # unless they respond to +to_io+ or +to_path+
24   def call(env)
25     status, headers, body = response = app.call(env)
27     # totally uninteresting to us if there's no body
28     if STATUS_WITH_NO_ENTITY_BODY.include?(status.to_i) ||
29        File === body ||
30        (body.respond_to?(:to_path) && File.file?(body.to_path))
31       return response
32     end
34     io = body.to_io if body.respond_to?(:to_io)
35     io ||= File.open(body.to_path) if body.respond_to?(:to_path)
36     return response if io.nil?
38     headers = Rack::Utils::HeaderHash.new(headers) unless Hash === headers
39     st = io.stat
40     fileno = io.fileno
41     FD_MAP[fileno] = io
42     if st.file?
43       headers[Content_Length] ||= st.size.to_s
44       headers.delete(Transfer_Encoding)
45     elsif st.pipe? || st.socket? # epoll-able things
46       unless headers.include?(Content_Length)
47         if env[Rainbows_autochunk]
48           case env[HTTP_VERSION]
49           when "HTTP/1.0", nil
50           else
51             headers[Transfer_Encoding] = Chunked
52           end
53         else
54           env[Rainbows_autochunk] = false
55         end
56       end
58       # we need to make sure our pipe output is Fiber-compatible
59       case env[Rainbows_model]
60       when :FiberSpawn, :FiberPool, :RevFiberSpawn, :CoolioFiberSpawn
61         io.respond_to?(:kgio_wait_readable) or
62           io = Rainbows::Fiber::IO.new(io)
63       when :Revactor
64         io = Rainbows::Revactor::Proxy.new(io)
65       end
66     else # unlikely, char/block device file, directory, ...
67       return response
68     end
69     [ status, headers, Body.new(io, "/dev/fd/#{fileno}", body) ]
70   end
72   class Body < Struct.new(:to_io, :to_path, :orig_body) # :nodoc:
73     # called by the webserver or other middlewares if they can't
74     # handle #to_path
75     def each
76       to_io.each { |x| yield x }
77     end
79     # remain Rack::Lint-compatible for people with wonky systems :P
80     unless File.directory?("/dev/fd")
81       alias to_path_orig to_path
82       undef_method :to_path
83     end
85     # called by the web server after #each
86     def close
87       to_io.close unless to_io.closed?
88       orig_body.close if orig_body.respond_to?(:close) # may not be an IO
89     rescue IOError # could've been IO::new()'ed and closed
90     end
91   end
92   #:startdoc:
93 end # class