Avoid freezing objects that don't benefit from it
[unicorn.git] / lib / unicorn / app / exec_cgi.rb
blob4f9216bba52984d252c73d4a2bda5d25091b5c32
1 # -*- encoding: binary -*-
3 require 'unicorn'
5 module Unicorn::App
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)
11     CHUNK_SIZE = 16384
12     PASS_VARS = %w(
13       CONTENT_LENGTH
14       CONTENT_TYPE
15       GATEWAY_INTERFACE
16       AUTH_TYPE
17       PATH_INFO
18       PATH_TRANSLATED
19       QUERY_STRING
20       REMOTE_ADDR
21       REMOTE_HOST
22       REMOTE_IDENT
23       REMOTE_USER
24       REQUEST_METHOD
25       SERVER_NAME
26       SERVER_PORT
27       SERVER_PROTOCOL
28       SERVER_SOFTWARE
29     ).map { |x| x.freeze } # frozen strings are faster for Hash assignments
31     # Intializes the app, example of usage in a config.ru
32     #   map "/cgit" do
33     #     run Unicorn::App::ExecCgi.new("/path/to/cgit.cgi")
34     #   end
35     def initialize(*args)
36       self.args = args
37       first = args[0] or
38         raise ArgumentError, "need path to executable"
39       first[0..0] == "/" or args[0] = ::File.expand_path(first)
40       File.executable?(args[0]) or
41         raise ArgumentError, "#{args[0]} is not executable"
42     end
44     # Calls the app
45     def call(env)
46       out, err = Unicorn::Util.tmpio, Unicorn::Util.tmpio
47       inp = force_file_input(env)
48       pid = fork { run_child(inp, out, err, env) }
49       inp.close
50       pid, status = Process.waitpid2(pid)
51       write_errors(env, err, status) if err.stat.size > 0
52       err.close
54       return parse_output!(out) if status.success?
55       out.close
56       [ 500, { 'Content-Length' => '0', 'Content-Type' => 'text/plain' }, [] ]
57     end
59     private
61     def run_child(inp, out, err, env)
62       PASS_VARS.each do |key|
63         val = env[key] or next
64         ENV[key] = val
65       end
66       ENV['SCRIPT_NAME'] = args[0]
67       ENV['GATEWAY_INTERFACE'] = 'CGI/1.1'
68       env.keys.grep(/^HTTP_/) { |key| ENV[key] = env[key] }
70       a = IO.new(0).reopen(inp)
71       b = IO.new(1).reopen(out)
72       c = IO.new(2).reopen(err)
73       exec(*args)
74     end
76     # Extracts headers from CGI out, will change the offset of out.
77     # This returns a standard Rack-compatible return value:
78     #   [ 200, HeadersHash, body ]
79     def parse_output!(out)
80       size = out.stat.size
81       out.sysseek(0)
82       head = out.sysread(CHUNK_SIZE)
83       offset = 2
84       head, body = head.split(/\n\n/, 2)
85       if body.nil?
86         head, body = head.split(/\r\n\r\n/, 2)
87         offset = 4
88       end
89       offset += head.length
91       # Allows +out+ to be used as a Rack body.
92       out.instance_eval { class << self; self; end }.instance_eval {
93         define_method(:each) { |&blk|
94           sysseek(offset)
96           # don't use a preallocated buffer for sysread since we can't
97           # guarantee an actual socket is consuming the yielded string
98           # (or if somebody is pushing to an array for eventual concatenation
99           begin
100             blk.call(sysread(CHUNK_SIZE))
101           rescue EOFError
102             break
103           end while true
104         }
105       }
107       size -= offset
108       prev = nil
109       headers = Rack::Utils::HeaderHash.new
110       head.split(/\r?\n/).each do |line|
111         case line
112         when /^([A-Za-z0-9-]+):\s*(.*)$/ then headers[prev = $1] = $2
113         when /^[ \t]/ then headers[prev] << "\n#{line}" if prev
114         end
115       end
116       headers['Content-Length'] = size.to_s
117       [ 200, headers, out ]
118     end
120     # ensures rack.input is a file handle that we can redirect stdin to
121     def force_file_input(env)
122       inp = env['rack.input']
123       if inp.size == 0 # inp could be a StringIO or StringIO-like object
124         ::File.open('/dev/null', 'rb')
125       else
126         tmp = Unicorn::Util.tmpio
128         buf = Unicorn::Z.dup
129         while inp.read(CHUNK_SIZE, buf)
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