Skip to content
This repository was archived by the owner on Dec 2, 2018. It is now read-only.

Commit 3b52ba0

Browse files
committed
Switch CGI implementation to use ChildProcess for the following reasons:
* We are already using it for the PHP process management * The API of ChildProcess is simpler than using popen, etc manually. * It offer cross platform support. * It offers more efficient mechanisms like POSIX_SPAWN which can even be enabled via the CHILDPROCES_POSIX_SPAWN environment variable.
1 parent 14ae71b commit 3b52ba0

File tree

2 files changed

+62
-75
lines changed

2 files changed

+62
-75
lines changed

lib/rack/legacy/cgi.rb

Lines changed: 37 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
require 'shellwords'
2-
31
class Rack::Legacy::Cgi
42

53
# Will setup a new instance of the CGI middleware executing
@@ -33,69 +31,52 @@ def valid?(path)
3331

3432
# Will run the given path with the given environment
3533
def run env, path
36-
env['DOCUMENT_ROOT'] = @public_dir
37-
env['SERVER_SOFTWARE'] = 'Rack Legacy'
38-
status = 200
39-
headers = {}
40-
body = ''
34+
# Setup CGI process
35+
cgi = ChildProcess.build path
36+
cgi.duplex = true
37+
cgi.cwd = File.dirname path
4138

42-
IO.popen('-', 'r+') do |io|
43-
if io.nil? # Child
44-
# Pass on all uppercase environment variables. Only uppercase
45-
# since Rack uses lower case ones internally.
46-
env.each do |key, value|
47-
ENV[key] = value if
48-
value.respond_to?(:to_str) && key =~ /^[A-Z_]+$/
49-
end
50-
exec path
51-
else # Parent
52-
# Send request to CGI sub-process
53-
io.write(env['rack.input'].read) if env['rack.input']
54-
io.close_write
55-
56-
# Parse headers coming back from sub-process
57-
until io.eof? || (line = io.readline.chomp) == ''
58-
if line =~ /\s*\:\s*/
59-
key, value = line.split(/\s*\:\s*/, 2)
60-
if headers.has_key? key
61-
headers[key] += "\n" + value
62-
else
63-
headers[key] = value
64-
end
65-
end
66-
end
39+
# Arrange CGI processes IO
40+
cgi_out, cgi.io.stdout = IO.pipe
41+
cgi.io.stderr = $stderr
6742

68-
# Get response and wait for process to complete.
69-
body = io.read
70-
Process.wait
43+
# Config CGI environment
44+
cgi.environment['DOCUMENT_ROOT'] = @public_dir
45+
cgi.environment['SERVER_SOFTWARE'] = 'Rack Legacy'
46+
env.each do |key, value|
47+
cgi.environment[key] = value if
48+
value.respond_to?(:to_str) && key =~ /^[A-Z_]+$/
49+
end
7150

72-
# If there was an error throw it up the execution stack so
73-
# someone can rescue to provide info to the right person.
74-
unless $?.exitstatus == 0
75-
# Build full command for easier debugging. Output to
76-
# stderr to prevent user from getting too much information.
77-
cmd = env.inject([path]) do |assignments, (key, value)|
78-
assignments.unshift "#{key}=#{value.to_s.shellescape}" if
79-
value.respond_to?(:to_str) && key =~ /^[A-Z_]+$/
80-
assignments
81-
end * ' '
82-
$stderr.puts <<ERROR
83-
CGI exited with status #{$?.exitstatus}. The full command run was:
51+
# Start running CGI
52+
cgi.start
8453

85-
#{cmd}
54+
# Delegate IO to CGI process
55+
cgi.io.stdin.write env['rack.input'].read if env['rack.input']
56+
cgi.io.stdout.close
8657

87-
ERROR
88-
raise Rack::Legacy::ExecutionError
58+
# Extract headers from output
59+
headers = {}
60+
until cgi_out.eof? || (line = cgi_out.readline.chomp) == ''
61+
if line =~ /\s*\:\s*/
62+
key, value = line.split /\s*\:\s*/, 2
63+
if headers.has_key? key
64+
headers[key] += "\n" + value
65+
else
66+
headers[key] = value
8967
end
90-
9168
end
9269
end
9370

94-
# Extract status from sub-process if it is doing something other
95-
# than a 200 response.
96-
status = headers.delete('Status').to_i if headers.has_key? 'Status'
71+
# Extract status from sub-process, default to 200
72+
status = (headers.delete('Status') || 200).to_i
73+
74+
# Throw error if process crashed.
75+
# NOTE: Process could still be running and crash later. This just
76+
# ensure we response correctly if it immmediately crashes
77+
raise Rack::Legacy::ExecutionError if cgi.crashed?
9778

98-
# Send it all back to rack
99-
[status, headers, [body]]
79+
# Send status, headers and remaining IO back to rack
80+
[status, headers, cgi_out]
10081
end
10182
end

test/unit/cgi_test.rb

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,40 +15,40 @@ def test_valid?
1515

1616
def test_call
1717
assert_equal \
18-
[200, {"Content-Type"=>"text/html", "Content-Length"=>"7"}, ['Success']],
19-
app.call({'PATH_INFO' => 'success.cgi', 'REQUEST_METHOD' => 'GET'})
18+
[200, {"Content-Type"=>"text/html", "Content-Length"=>"7"}, 'Success'],
19+
call({'PATH_INFO' => 'success.cgi', 'REQUEST_METHOD' => 'GET'})
2020
assert_equal \
2121
[200, {"Content-Type"=>"text/html"}, 'Endpoint'],
22-
app.call({'PATH_INFO' => 'missing.cgi'})
23-
assert_equal [200, {}, ['']],
24-
app.call({'PATH_INFO' => 'empty.cgi', 'REQUEST_METHOD' => 'GET'})
25-
assert_equal [404, {"Content-Type"=>"text/html"}, ['']],
26-
app.call({'PATH_INFO' => '404.cgi', 'REQUEST_METHOD' => 'GET'})
27-
assert_equal [200, {"Content-Type"=>"text/html", 'Set-Cookie' => "cookie1\ncookie2"}, ['']],
28-
app.call({'PATH_INFO' => 'dup_headers.cgi', 'REQUEST_METHOD' => 'GET'})
22+
call({'PATH_INFO' => 'missing.cgi'})
23+
assert_equal [200, {}, ''],
24+
call({'PATH_INFO' => 'empty.cgi', 'REQUEST_METHOD' => 'GET'})
25+
assert_equal [404, {"Content-Type"=>"text/html"}, ''],
26+
call({'PATH_INFO' => '404.cgi', 'REQUEST_METHOD' => 'GET'})
27+
assert_equal [200, {"Content-Type"=>"text/html", 'Set-Cookie' => "cookie1\ncookie2"}, ''],
28+
call({'PATH_INFO' => 'dup_headers.cgi', 'REQUEST_METHOD' => 'GET'})
2929

3030
assert_raises Rack::Legacy::ExecutionError do
3131
$stderr.reopen open('/dev/null', 'w')
32-
app.call({'PATH_INFO' => 'error.cgi', 'REQUEST_METHOD' => 'GET'})
32+
call({'PATH_INFO' => 'error.cgi', 'REQUEST_METHOD' => 'GET'})
3333
$stderr.reopen STDERR
3434
end
3535

3636
assert_raises Rack::Legacy::ExecutionError do
3737
$stderr.reopen open('/dev/null', 'w')
38-
app.call({'PATH_INFO' => 'syntax_error.cgi', 'REQUEST_METHOD' => 'GET'})
38+
call({'PATH_INFO' => 'syntax_error.cgi', 'REQUEST_METHOD' => 'GET'})
3939
$stderr.reopen STDERR
4040
end
4141

4242
assert_equal \
43-
[200, {"Content-Type"=>"text/html", "Content-Length"=>"5"}, ['query']],
44-
app.call({
43+
[200, {"Content-Type"=>"text/html", "Content-Length"=>"5"}, 'query'],
44+
call({
4545
'PATH_INFO' => 'param.cgi',
4646
'QUERY_STRING' => 'q=query',
4747
'REQUEST_METHOD' => 'GET'
4848
})
4949
assert_equal \
50-
[200, {"Content-Type"=>"text/html", "Content-Length"=>"4"}, ['post']],
51-
app.call({
50+
[200, {"Content-Type"=>"text/html", "Content-Length"=>"4"}, 'post'],
51+
call({
5252
'PATH_INFO' => 'param.cgi',
5353
'REQUEST_METHOD' => 'POST',
5454
'CONTENT_LENGTH' => '6',
@@ -62,8 +62,8 @@ def test_call
6262
end
6363

6464
def test_environment
65-
status, headers, body = *app.call({'PATH_INFO' => 'env.cgi', 'REQUEST_METHOD' => 'GET'})
66-
env = eval(body[0])
65+
status, headers, body = *call({'PATH_INFO' => 'env.cgi', 'REQUEST_METHOD' => 'GET'})
66+
env = eval body
6767
assert File.join(File.dirname(__FILE__), '../fixtures'), env['DOCUMENT_ROOT']
6868
assert 'Rack Legacy', env['SERVER_SOFTWARE']
6969
end
@@ -74,9 +74,15 @@ def fixture_file path
7474
File.expand_path path, File.join(File.dirname(__FILE__), '../fixtures')
7575
end
7676

77+
def call env
78+
status, headers, body = *app.call(env)
79+
body = body.read
80+
[status, headers, body]
81+
end
82+
7783
def app
78-
Rack::Legacy::Cgi.new \
79-
proc {[200, {'Content-Type' => 'text/html'}, 'Endpoint']},
84+
@app ||= Rack::Legacy::Cgi.new \
85+
proc {[200, {'Content-Type' => 'text/html'}, StringIO.new('Endpoint')]},
8086
File.join(File.dirname(__FILE__), '../fixtures')
8187
end
8288

0 commit comments

Comments
 (0)