1 # -*- encoding: binary -*-
7 # This class is highly experimental (even more so than the rest of Unicorn)
8 # and has never run anything other than cgit.
9 class ExecCgi < Struct.new(:args)
29 ).map { |x| x.freeze } # frozen strings are faster for Hash assignments
31 class Body < Unicorn::TmpIO
33 sysseek(@body_offset = n)
38 # don't use a preallocated buffer for sysread since we can't
39 # guarantee an actual socket is consuming the yielded string
40 # (or if somebody is pushing to an array for eventual concatenation
42 yield sysread(CHUNK_SIZE)
49 # Intializes the app, example of usage in a config.ru
51 # run Unicorn::App::ExecCgi.new("/path/to/cgit.cgi")
56 raise ArgumentError, "need path to executable"
57 first[0] == ?/ or args[0] = ::File.expand_path(first)
58 File.executable?(args[0]) or
59 raise ArgumentError, "#{args[0]} is not executable"
64 out, err = Body.new, Unicorn::TmpIO.new
65 inp = force_file_input(env)
66 pid = fork { run_child(inp, out, err, env) }
68 pid, status = Process.waitpid2(pid)
69 write_errors(env, err, status) if err.stat.size > 0
72 return parse_output!(out) if status.success?
74 [ 500, { 'Content-Length' => '0', 'Content-Type' => 'text/plain' }, [] ]
79 def run_child(inp, out, err, env)
80 PASS_VARS.each do |key|
81 val = env[key] or next
84 ENV['SCRIPT_NAME'] = args[0]
85 ENV['GATEWAY_INTERFACE'] = 'CGI/1.1'
86 env.keys.grep(/^HTTP_/) { |key| ENV[key] = env[key] }
94 # Extracts headers from CGI out, will change the offset of out.
95 # This returns a standard Rack-compatible return value:
96 # [ 200, HeadersHash, body ]
97 def parse_output!(out)
100 head = out.sysread(CHUNK_SIZE)
102 head, body = head.split(/\n\n/, 2)
104 head, body = head.split(/\r\n\r\n/, 2)
107 offset += head.length
108 out.body_offset = offset
111 headers = Rack::Utils::HeaderHash.new
112 head.split(/\r?\n/).each do |line|
114 when /^([A-Za-z0-9-]+):\s*(.*)$/ then headers[prev = $1] = $2
115 when /^[ \t]/ then headers[prev] << "\n#{line}" if prev
118 status = headers.delete("Status") || 200
119 headers['Content-Length'] = size.to_s
120 [ status, headers, out ]
123 # ensures rack.input is a file handle that we can redirect stdin to
124 def force_file_input(env)
125 inp = env['rack.input']
126 # inp could be a StringIO or StringIO-like object
127 if inp.respond_to?(:size) && inp.size == 0
128 ::File.open('/dev/null', 'rb')
130 tmp = Unicorn::TmpIO.new
132 buf = inp.read(CHUNK_SIZE)
135 end while inp.read(CHUNK_SIZE, buf)
141 # rack.errors this may not be an IO object, so we couldn't
142 # just redirect the CGI executable to that earlier.
143 def write_errors(env, err, status)
145 dst = env['rack.errors']
147 dst.write("#{pid}: #{args.inspect} status=#{status} stderr:\n")
148 err.each_line { |line| dst.write("#{pid}: #{line}") }