Simplify sendfile logic

A safe and reliable check for whether a file descriptor supports mmap
is to directly check if it is seekable. However, some seekable file
descriptors may also report a zero size when calling fstat. If there
is no content length specified for the response and it cannot be
determined from the file descriptor then it is not possible to know
what chunk size to send to the client. In this case, is it necessary
to fall back to unwinding the body by iteration.

The above conditions together reveal a straightforward and reliable
way to check for sendfile support. This patch modifies the Response
class to assert these conditions using a try/catch block as part of
a new, simplified sendfile method. This method returns False if it
is not possible to serve the response using sendfile. Otherwise, it
serves the response and returns True. By returning False when SSL is
in use, the code is made even simpler by removing the special support
for SSL, which is served well enough by the iteration protocol.

Fix #1038
This commit is contained in:
Randall Leeds 2015-08-05 16:49:43 -07:00
parent 9158ab20f8
commit 18d2b92146
2 changed files with 38 additions and 75 deletions

View File

@ -12,7 +12,6 @@ import sys
from gunicorn._compat import unquote_to_wsgi_str from gunicorn._compat import unquote_to_wsgi_str
from gunicorn.six import string_types, binary_type, reraise from gunicorn.six import string_types, binary_type, reraise
from gunicorn import SERVER_SOFTWARE from gunicorn import SERVER_SOFTWARE
import gunicorn.six as six
import gunicorn.util as util import gunicorn.util as util
try: try:
@ -24,6 +23,10 @@ except ImportError:
except ImportError: except ImportError:
sendfile = None sendfile = None
# Send files in at most 1GB blocks as some operating systems can have problems
# with sending files in blocks over 2GB.
BLKSIZE = 0x3FFFFFFF
NORMALIZE_SPACE = re.compile(r'(?:\r\n)?[ \t]+') NORMALIZE_SPACE = re.compile(r'(?:\r\n)?[ \t]+')
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -344,77 +347,51 @@ class Response(object):
util.write(self.sock, arg, self.chunked) util.write(self.sock, arg, self.chunked)
def can_sendfile(self): def can_sendfile(self):
return (self.cfg.sendfile and (sendfile is not None)) return self.cfg.sendfile and sendfile is not None
def sendfile_all(self, fileno, sockno, offset, nbytes): def sendfile(self, respiter):
# Send file in at most 1GB blocks as some operating if self.cfg.is_ssl or not self.can_sendfile():
# systems can have problems with sending files in blocks return False
# over 2GB.
BLKSIZE = 0x3FFFFFFF try:
fileno = respiter.filelike.fileno()
offset = os.lseek(fileno, 0, os.SEEK_CUR)
if self.response_length is None:
filesize = os.fstat(fileno).st_size
if nbytes > BLKSIZE: # The file may be special and sendfile will fail.
for m in range(0, nbytes, BLKSIZE): # It may also be zero-length, but that is okay.
self.sendfile_all(fileno, sockno, offset, min(nbytes, BLKSIZE)) if filesize == 0:
offset += BLKSIZE return False
nbytes -= BLKSIZE
else:
sent = 0
sent += sendfile(sockno, fileno, offset + sent, nbytes - sent)
while sent != nbytes:
sent += sendfile(sockno, fileno, offset + sent, nbytes - sent)
def sendfile_use_send(self, fileno, fo_offset, nbytes): nbytes = filesize - offset
else:
nbytes = self.response_length
except (OSError, io.UnsupportedOperation):
return False
# send file in blocks of 8182 bytes self.send_headers()
BLKSIZE = 8192
if self.is_chunked():
chunk_size = "%X\r\n" % nbytes
self.sock.sendall(chunk_size.encode('utf-8'))
sockno = self.sock.fileno()
sent = 0 sent = 0
while sent != nbytes:
data = os.read(fileno, BLKSIZE)
if not data:
break
sent += len(data) for m in range(0, nbytes, BLKSIZE):
if sent > nbytes: count = min(nbytes - sent, BLKSIZE)
data = data[:nbytes - sent] sent += sendfile(sockno, fileno, offset + sent, count)
util.write(self.sock, data, self.chunked) if self.is_chunked():
self.sock.sendall(b"\r\n")
os.lseek(fileno, offset, os.SEEK_SET)
return True
def write_file(self, respiter): def write_file(self, respiter):
if self.can_sendfile() and util.is_fileobject(respiter.filelike): if not self.sendfile(respiter):
# sometimes the fileno isn't a callable
if six.callable(respiter.filelike.fileno):
fileno = respiter.filelike.fileno()
else:
fileno = respiter.filelike.fileno
fd_offset = os.lseek(fileno, 0, os.SEEK_CUR)
fo_offset = respiter.filelike.tell()
nbytes = max(os.fstat(fileno).st_size - fo_offset, 0)
if self.response_length:
nbytes = min(nbytes, self.response_length)
if nbytes == 0:
return
self.send_headers()
if self.cfg.is_ssl:
self.sendfile_use_send(fileno, fo_offset, nbytes)
else:
if self.is_chunked():
chunk_size = "%X\r\n" % nbytes
self.sock.sendall(chunk_size.encode('utf-8'))
self.sendfile_all(fileno, self.sock.fileno(), fo_offset, nbytes)
if self.is_chunked():
self.sock.sendall(b"\r\n")
os.lseek(fileno, fd_offset, os.SEEK_SET)
else:
for item in respiter: for item in respiter:
self.write(item) self.write(item)

View File

@ -7,7 +7,6 @@ from __future__ import print_function
import email.utils import email.utils
import fcntl import fcntl
import io
import os import os
import pkg_resources import pkg_resources
import random import random
@ -517,19 +516,6 @@ def to_latin1(value):
return value.encode("latin-1") return value.encode("latin-1")
def is_fileobject(obj):
if not hasattr(obj, "tell") or not hasattr(obj, "fileno"):
return False
# check BytesIO case and maybe others
try:
obj.fileno()
except (IOError, io.UnsupportedOperation):
return False
return True
def warn(msg): def warn(msg):
print("!!!", file=sys.stderr) print("!!!", file=sys.stderr)