app/exec_cgi: GC prevention
[unicorn.git] / lib / unicorn / app / exec_cgi.rb
blobd98b3e4499ee0b5de9a22c002067535fa6e23798
1 require 'unicorn'
2 require 'rack'
4 module Unicorn::App
6   # This class is highly experimental (even more so than the rest of Unicorn)
7   # and has never run anything other than cgit.
8   class ExecCgi
10     CHUNK_SIZE = 16384
11     PASS_VARS = %w(
12       CONTENT_LENGTH
13       CONTENT_TYPE
14       GATEWAY_INTERFACE
15       AUTH_TYPE
16       PATH_INFO
17       PATH_TRANSLATED
18       QUERY_STRING
19       REMOTE_ADDR
20       REMOTE_HOST
21       REMOTE_IDENT
22       REMOTE_USER
23       REQUEST_METHOD
24       SERVER_NAME
25       SERVER_PORT
26       SERVER_PROTOCOL
27       SERVER_SOFTWARE
28     ).map { |x| x.freeze }.freeze # frozen strings are faster for Hash lookups
30     # Intializes the app, example of usage in a config.ru
31     #   map "/cgit" do
32     #     run Unicorn::App::ExecCgi.new("/path/to/cgit.cgi")
33     #   end
34     def initialize(*args)
35       @args = args.dup
36       first = @args[0] or
37         raise ArgumentError, "need path to executable"
38       first[0..0] == "/" or @args[0] = ::File.expand_path(first)
39       File.executable?(@args[0]) or
40         raise ArgumentError, "#{@args[0]} is not executable"
41     end
43     # Calls the app
44     def call(env)
45       out, err = Tempfile.new(''), Tempfile.new('')
46       out.unlink
47       err.unlink
48       inp = force_file_input(env)
49       inp.sync = out.sync = err.sync = true
50       pid = fork { run_child(inp, out, err, env) }
51       inp.close
52       pid, status = Process.waitpid2(pid)
53       write_errors(env, err, status) if err.stat.size > 0
54       err.close
56       return parse_output!(out) if status.success?
57       out.close
58       [ 500, { 'Content-Length' => '0', 'Content-Type' => 'text/plain' }, [] ]
59     end
61     private
63     def run_child(inp, out, err, env)
64       PASS_VARS.each do |key|
65         val = env[key] or next
66         ENV[key] = val
67       end
68       ENV['SCRIPT_NAME'] = @args[0]
69       ENV['GATEWAY_INTERFACE'] = 'CGI/1.1'
70       env.keys.grep(/^HTTP_/) { |key| ENV[key] = env[key] }
72       a = IO.new(0).reopen(inp)
73       b = IO.new(1).reopen(out)
74       c = IO.new(2).reopen(err)
75       exec(*@args)
76     end
78     # Extracts headers from CGI out, will change the offset of out.
79     # This returns a standard Rack-compatible return value:
80     #   [ 200, HeadersHash, body ]
81     def parse_output!(out)
82       size = out.stat.size
83       out.sysseek(0)
84       head = out.sysread(CHUNK_SIZE)
85       offset = 2
86       head, body = head.split(/\n\n/, 2)
87       if body.nil?
88         head, body = head.split(/\r\n\r\n/, 2)
89         offset = 4
90       end
91       offset += head.length
92       out.instance_variable_set('@unicorn_app_exec_cgi_offset', offset)
93       size -= offset
95       # Allows +out+ to be used as a Rack body.
96       def out.each
97         sysseek(@unicorn_app_exec_cgi_offset)
98         begin
99           loop { yield(sysread(CHUNK_SIZE)) }
100         rescue EOFError
101         end
102       end
104       prev = nil
105       headers = Rack::Utils::HeaderHash.new
106       head.split(/\r?\n/).each do |line|
107         case line
108         when /^([A-Za-z0-9-]+):\s*(.*)$/ then headers[prev = $1] = $2
109         when /^[ \t]/ then headers[prev] << "\n#{line}" if prev
110         end
111       end
112       headers['Content-Length'] = size.to_s
113       [ 200, headers, out ]
114     end
116     # ensures rack.input is a file handle that we can redirect stdin to
117     def force_file_input(env)
118       inp = env['rack.input']
119       if inp.respond_to?(:fileno) && Integer === inp.fileno
120         inp
121       elsif inp.size == 0 # inp could be a StringIO or StringIO-like object
122         ::File.open('/dev/null')
123       else
124         tmp = Tempfile.new('')
125         tmp.unlink
126         tmp.binmode
128         # Rack::Lint::InputWrapper doesn't allow sysread :(
129         while buf = inp.read(CHUNK_SIZE)
130           tmp.syswrite(buf)
131         end
132         tmp.sysseek(0)
133         tmp
134       end
135     end
137     # rack.errors this may not be an IO object, so we couldn't
138     # just redirect the CGI executable to that earlier.
139     def write_errors(env, err, status)
140       err.seek(0)
141       dst = env['rack.errors']
142       pid = status.pid
143       dst.write("#{pid}: #{@args.inspect} status=#{status} stderr:\n")
144       err.each_line { |line| dst.write("#{pid}: #{line}") }
145       dst.flush
146     end
148   end