Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed static files handling for loops without .sendfile() #5157

Merged
merged 1 commit into from
Oct 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES/5149.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed static files handling for loops without .sendfile()
1 change: 1 addition & 0 deletions CONTRIBUTORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ Vaibhav Sagar
Vamsi Krishna Avula
Vasiliy Faronov
Vasyl Baran
Viacheslav Greshilov
Victor Collod
Victor Kovtun
Victor Makarov
Expand Down
161 changes: 22 additions & 139 deletions aiohttp/web_fileresponse.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import mimetypes
import os
import pathlib
from functools import partial
import sys
from typing import ( # noqa
IO,
TYPE_CHECKING,
Expand All @@ -17,10 +17,6 @@

from . import hdrs
from .abc import AbstractStreamWriter
from .base_protocol import BaseProtocol
from .helpers import set_exception, set_result
from .http_writer import StreamWriter
from .log import server_logger
from .typedefs import LooseHeaders
from .web_exceptions import (
HTTPNotModified,
Expand All @@ -42,95 +38,6 @@
NOSENDFILE = bool(os.environ.get("AIOHTTP_NOSENDFILE"))


class SendfileStreamWriter(StreamWriter):
def __init__(
self,
protocol: BaseProtocol,
loop: asyncio.AbstractEventLoop,
fobj: IO[Any],
offset: int,
count: int,
on_chunk_sent: _T_OnChunkSent = None,
) -> None:
super().__init__(protocol, loop, on_chunk_sent)
self._sendfile_buffer = [] # type: List[bytes]
self._fobj = fobj
self._count = count
self._offset = offset
self._in_fd = fobj.fileno()

def _write(self, chunk: bytes) -> None:
# we overwrite StreamWriter._write, so nothing can be appended to
# _buffer, and nothing is written to the transport directly by the
# parent class
self.output_size += len(chunk)
self._sendfile_buffer.append(chunk)

def _sendfile_cb(self, fut: "asyncio.Future[None]", out_fd: int) -> None:
if fut.cancelled():
return
try:
if self._do_sendfile(out_fd):
set_result(fut, None)
except Exception as exc:
set_exception(fut, exc)

def _do_sendfile(self, out_fd: int) -> bool:
try:
n = os.sendfile(out_fd, self._in_fd, self._offset, self._count)
if n == 0: # in_fd EOF reached
n = self._count
except (BlockingIOError, InterruptedError):
n = 0
self.output_size += n
self._offset += n
self._count -= n
assert self._count >= 0
return self._count == 0

def _done_fut(self, out_fd: int, fut: "asyncio.Future[None]") -> None:
self.loop.remove_writer(out_fd)

async def sendfile(self) -> None:
assert self.transport is not None
loop = self.loop
data = b"".join(self._sendfile_buffer)
if hasattr(loop, "sendfile"):
# Python 3.7+
self.transport.write(data)
if self._count != 0:
await loop.sendfile(
self.transport, self._fobj, self._offset, self._count
)
await super().write_eof()
return

self._fobj.seek(self._offset)
out_socket = self.transport.get_extra_info("socket").dup()
out_socket.setblocking(False)
out_fd = out_socket.fileno()

try:
await loop.sock_sendall(out_socket, data)
if not self._do_sendfile(out_fd):
fut = loop.create_future()
fut.add_done_callback(partial(self._done_fut, out_fd))
loop.add_writer(out_fd, self._sendfile_cb, fut, out_fd)
await fut
except asyncio.CancelledError:
raise
except Exception:
server_logger.debug("Socket error")
self.transport.close()
finally:
out_socket.close()

await super().write_eof()

async def write_eof(self, chunk: bytes = b"") -> None:
pass


class FileResponse(StreamResponse):
"""A response object can be used to send files."""

Expand All @@ -150,52 +57,12 @@ def __init__(
self._path = path
self._chunk_size = chunk_size

async def _sendfile_system(
self, request: "BaseRequest", fobj: IO[Any], offset: int, count: int
) -> AbstractStreamWriter:
# Write count bytes of fobj to resp using
# the os.sendfile system call.
#
# For details check
# https:/KeepSafe/aiohttp/issues/1177
# See https:/KeepSafe/aiohttp/issues/958 for details
#
# request should be an aiohttp.web.Request instance.
# fobj should be an open file object.
# count should be an integer > 0.

transport = request.transport
assert transport is not None
if (
transport.get_extra_info("sslcontext")
or transport.get_extra_info("socket") is None
or self.compression
):
writer = await self._sendfile_fallback(request, fobj, offset, count)
else:
writer = SendfileStreamWriter(
request.protocol, request._loop, fobj, offset, count
)
request._payload_writer = writer

await super().prepare(request)
await writer.sendfile()

return writer

async def _sendfile_fallback(
self, request: "BaseRequest", fobj: IO[Any], offset: int, count: int
self, writer: AbstractStreamWriter, fobj: IO[Any], offset: int, count: int
) -> AbstractStreamWriter:
# Mimic the _sendfile_system() method, but without using the
# os.sendfile() system call. This should be used on systems
# that don't support the os.sendfile().

# To keep memory usage low,fobj is transferred in chunks
# controlled by the constructor's chunk_size argument.

writer = await super().prepare(request)
assert writer is not None

chunk_size = self._chunk_size
loop = asyncio.get_event_loop()

Expand All @@ -212,10 +79,26 @@ async def _sendfile_fallback(
await writer.drain()
return writer

if hasattr(os, "sendfile") and not NOSENDFILE: # pragma: no cover
_sendfile = _sendfile_system
else: # pragma: no cover
_sendfile = _sendfile_fallback
async def _sendfile(
self, request: "BaseRequest", fobj: IO[Any], offset: int, count: int
) -> AbstractStreamWriter:
writer = await super().prepare(request)
assert writer is not None

if NOSENDFILE or sys.version_info < (3, 7) or self.compression:
return await self._sendfile_fallback(writer, fobj, offset, count)

loop = request._loop
transport = request.transport
assert transport is not None

try:
await loop.sendfile(transport, fobj, offset, count)
except NotImplementedError:
return await self._sendfile_fallback(writer, fobj, offset, count)

await super().write_eof()
return writer

async def prepare(self, request: "BaseRequest") -> Optional[AbstractStreamWriter]:
filepath = self._path
Expand Down
17 changes: 13 additions & 4 deletions tests/test_web_sendfile_functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,21 @@
ssl = None # type: ignore


@pytest.fixture(params=["sendfile", "fallback"], ids=["sendfile", "fallback"])
def sender(request):
@pytest.fixture
def loop_without_sendfile(loop):
def sendfile(*args, **kwargs):
raise NotImplementedError

loop.sendfile = sendfile
return loop


@pytest.fixture(params=["sendfile", "no_sendfile"], ids=["sendfile", "no_sendfile"])
def sender(request, loop_without_sendfile):
def maker(*args, **kwargs):
ret = web.FileResponse(*args, **kwargs)
if request.param == "fallback":
ret._sendfile = ret._sendfile_fallback
if request.param == "no_sendfile":
asyncio.set_event_loop(loop_without_sendfile)
return ret

return maker
Expand Down