Skip to content

Commit a773018

Browse files
committed
[3.14] gh-142176: Read/write CGI data using worker threads
This reads/writes data as available, making the CGI application responsible for managing any timeouts when receiving data from clients. Data is read in chunks of bounded size, and passed on immediately (except stderr, which is combined into a single message as before). This does need 3 threads. (As does process.communicate.)
1 parent b053c2a commit a773018

File tree

1 file changed

+54
-21
lines changed

1 file changed

+54
-21
lines changed

Lib/http/server.py

Lines changed: 54 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@
104104
import socket
105105
import socketserver
106106
import sys
107+
import threading
107108
import time
108109
import urllib.parse
109110

@@ -136,7 +137,7 @@
136137

137138
# Data larger than this will be read in chunks, to prevent extreme
138139
# overallocation.
139-
_MIN_READ_BUF_SIZE = 1 << 20
140+
_READ_BUF_SIZE = 1 << 20
140141

141142
class HTTPServer(socketserver.TCPServer):
142143

@@ -1287,30 +1288,62 @@ def run_cgi(self):
12871288
stderr=subprocess.PIPE,
12881289
env = env
12891290
)
1291+
def finish_request():
1292+
# throw away additional data [see bug #427345, gh-34546]
1293+
while select.select([self.rfile._sock], [], [], 0)[0]:
1294+
if not self.rfile._sock.recv(1):
1295+
break
12901296
if self.command.lower() == "post" and nbytes > 0:
1291-
cursize = 0
1292-
data = self.rfile.read(min(nbytes, _MIN_READ_BUF_SIZE))
1293-
while (len(data) < nbytes and len(data) != cursize and
1294-
select.select([self.rfile._sock], [], [], 0)[0]):
1295-
cursize = len(data)
1296-
# This is a geometric increase in read size (never more
1297-
# than doubling our the current length of data per loop
1298-
# iteration).
1299-
delta = min(cursize, nbytes - cursize)
1300-
data += self.rfile.read(delta)
1297+
def _in_task():
1298+
"""Pipe the input into the process stdin"""
1299+
bytes_left = nbytes
1300+
# We need to wait until either there's new data in rfile,
1301+
# or the process has exited.
1302+
# This spins (with short sleeps) polling for process exit.
1303+
TIMEOUT = 0.1
1304+
while (
1305+
bytes_left
1306+
and not p.returncode
1307+
and select.select([self.rfile._sock], [], [], TIMEOUT)[0]
1308+
):
1309+
data = self.rfile.read(min(bytes_left, _READ_BUF_SIZE))
1310+
if not data:
1311+
break
1312+
bytes_left -= len(data)
1313+
p.stdin.write(data)
1314+
finish_request()
1315+
try:
1316+
p.stdin.close()
1317+
except OSError:
1318+
# already closed
1319+
pass
1320+
request_relay_thread = threading.Thread(target=_in_task)
1321+
request_relay_thread.start()
13011322
else:
13021323
data = None
1303-
# throw away additional data [see bug #427345]
1304-
while select.select([self.rfile._sock], [], [], 0)[0]:
1305-
if not self.rfile._sock.recv(1):
1306-
break
1307-
stdout, stderr = p.communicate(data)
1308-
self.wfile.write(stdout)
1309-
if stderr:
1310-
self.log_error('%s', stderr)
1311-
p.stderr.close()
1324+
finish_request()
1325+
request_relay_thread = None
1326+
def _out_task():
1327+
"""Pipe the process's stdout into the socket"""
1328+
while data := p.stdout.read(_READ_BUF_SIZE):
1329+
self.wfile.write(data)
1330+
response_relay_thread = threading.Thread(target=_out_task)
1331+
response_relay_thread.start()
1332+
stderr_chunks = []
1333+
def _err_task():
1334+
"""Collect all of stderr, to log as single message"""
1335+
while data := p.stderr.read(_READ_BUF_SIZE):
1336+
stderr_chunks.append(data)
1337+
error_log_thread = threading.Thread(target=_err_task)
1338+
error_log_thread.start()
1339+
status = p.wait()
1340+
response_relay_thread.join()
13121341
p.stdout.close()
1313-
status = p.returncode
1342+
error_log_thread.join()
1343+
self.log_error('%s', b''.join(stderr_chunks))
1344+
p.stderr.close()
1345+
if request_relay_thread:
1346+
request_relay_thread.join()
13141347
if status:
13151348
self.log_error("CGI script exit status %#x", status)
13161349
else:

0 commit comments

Comments
 (0)