6 # This class is highly experimental (even more so than the rest of Unicorn)
7 # and has never run anything other than cgit.
28 ).map { |x| x.freeze }.freeze # frozen strings are faster for Hash lookups
30 # Intializes the app, example of usage in a config.ru
32 # run Unicorn::App::ExecCgi.new("/path/to/cgit.cgi")
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"
45 out, err = Tempfile.new(''), Tempfile.new('')
48 inp = force_file_input(env)
49 inp.sync = out.sync = err.sync = true
50 pid = fork { run_child(inp, out, err, env) }
52 pid, status = Process.waitpid2(pid)
53 write_errors(env, err, status) if err.stat.size > 0
56 return parse_output!(out) if status.success?
58 [ 500, { 'Content-Length' => '0', 'Content-Type' => 'text/plain' }, [] ]
63 def run_child(inp, out, err, env)
64 PASS_VARS.each do |key|
65 val = env[key] or next
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)
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)
84 head = out.sysread(CHUNK_SIZE)
86 head, body = head.split(/\n\n/, 2)
88 head, body = head.split(/\r\n\r\n/, 2)
92 out.instance_variable_set('@unicorn_app_exec_cgi_offset', offset)
95 # Allows +out+ to be used as a Rack body.
97 sysseek(@unicorn_app_exec_cgi_offset)
99 loop { yield(sysread(CHUNK_SIZE)) }
105 headers = Rack::Utils::HeaderHash.new
106 head.split(/\r?\n/).each do |line|
108 when /^([A-Za-z0-9-]+):\s*(.*)$/ then headers[prev = $1] = $2
109 when /^[ \t]/ then headers[prev] << "\n#{line}" if prev
112 headers['Content-Length'] = size.to_s
113 [ 200, headers, out ]
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
121 elsif inp.size == 0 # inp could be a StringIO or StringIO-like object
122 ::File.open('/dev/null')
124 tmp = Tempfile.new('')
128 # Rack::Lint::InputWrapper doesn't allow sysread :(
129 while buf = inp.read(CHUNK_SIZE)
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)
141 dst = env['rack.errors']
143 dst.write("#{pid}: #{@args.inspect} status=#{status} stderr:\n")
144 err.each_line { |line| dst.write("#{pid}: #{line}") }