mirror of
https://github.com/python/cpython.git
synced 2025-11-01 18:51:43 +00:00
Issue 10882: add os.sendfile(). (patch provided by Ross Lagerwall)
This commit is contained in:
parent
59db1f3df6
commit
c9c2c8b034
7 changed files with 550 additions and 21 deletions
|
|
@ -15,6 +15,13 @@ from test import support
|
|||
import contextlib
|
||||
import mmap
|
||||
import uuid
|
||||
import asyncore
|
||||
import asynchat
|
||||
import socket
|
||||
try:
|
||||
import threading
|
||||
except ImportError:
|
||||
threading = None
|
||||
|
||||
# Detect whether we're on a Linux system that uses the (now outdated
|
||||
# and unmaintained) linuxthreads threading library. There's an issue
|
||||
|
|
@ -1261,6 +1268,251 @@ class LoginTests(unittest.TestCase):
|
|||
self.assertNotEqual(len(user_name), 0)
|
||||
|
||||
|
||||
class SendfileTestServer(asyncore.dispatcher, threading.Thread):
|
||||
|
||||
class Handler(asynchat.async_chat):
|
||||
|
||||
def __init__(self, conn):
|
||||
asynchat.async_chat.__init__(self, conn)
|
||||
self.in_buffer = []
|
||||
self.closed = False
|
||||
self.push(b"220 ready\r\n")
|
||||
|
||||
def handle_read(self):
|
||||
data = self.recv(4096)
|
||||
self.in_buffer.append(data)
|
||||
|
||||
def get_data(self):
|
||||
return b''.join(self.in_buffer)
|
||||
|
||||
def handle_close(self):
|
||||
self.close()
|
||||
self.closed = True
|
||||
|
||||
def handle_error(self):
|
||||
raise
|
||||
|
||||
def __init__(self, address):
|
||||
threading.Thread.__init__(self)
|
||||
asyncore.dispatcher.__init__(self)
|
||||
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.bind(address)
|
||||
self.listen(5)
|
||||
self.host, self.port = self.socket.getsockname()[:2]
|
||||
self.handler_instance = None
|
||||
self._active = False
|
||||
self._active_lock = threading.Lock()
|
||||
|
||||
# --- public API
|
||||
|
||||
@property
|
||||
def running(self):
|
||||
return self._active
|
||||
|
||||
def start(self):
|
||||
assert not self.running
|
||||
self.__flag = threading.Event()
|
||||
threading.Thread.start(self)
|
||||
self.__flag.wait()
|
||||
|
||||
def stop(self):
|
||||
assert self.running
|
||||
self._active = False
|
||||
self.join()
|
||||
|
||||
def wait(self):
|
||||
# wait for handler connection to be closed, then stop the server
|
||||
while not getattr(self.handler_instance, "closed", True):
|
||||
time.sleep(0.001)
|
||||
self.stop()
|
||||
|
||||
# --- internals
|
||||
|
||||
def run(self):
|
||||
self._active = True
|
||||
self.__flag.set()
|
||||
while self._active and asyncore.socket_map:
|
||||
self._active_lock.acquire()
|
||||
asyncore.loop(timeout=0.001, count=1)
|
||||
self._active_lock.release()
|
||||
asyncore.close_all()
|
||||
|
||||
def handle_accept(self):
|
||||
conn, addr = self.accept()
|
||||
self.handler_instance = self.Handler(conn)
|
||||
|
||||
def handle_connect(self):
|
||||
self.close()
|
||||
handle_read = handle_connect
|
||||
|
||||
def writable(self):
|
||||
return 0
|
||||
|
||||
def handle_error(self):
|
||||
raise
|
||||
|
||||
|
||||
@unittest.skipUnless(hasattr(os, 'sendfile'), "test needs os.sendfile()")
|
||||
class TestSendfile(unittest.TestCase):
|
||||
|
||||
DATA = b"12345abcde" * 1024 * 1024 # 10 Mb
|
||||
SUPPORT_HEADERS_TRAILERS = not sys.platform.startswith("linux") and \
|
||||
not sys.platform.startswith("solaris")
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
with open(support.TESTFN, "wb") as f:
|
||||
f.write(cls.DATA)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
support.unlink(support.TESTFN)
|
||||
|
||||
def setUp(self):
|
||||
self.server = SendfileTestServer((support.HOST, 0))
|
||||
self.server.start()
|
||||
self.client = socket.socket()
|
||||
self.client.connect((self.server.host, self.server.port))
|
||||
self.client.settimeout(1)
|
||||
# synchronize by waiting for "220 ready" response
|
||||
self.client.recv(1024)
|
||||
self.sockno = self.client.fileno()
|
||||
self.file = open(support.TESTFN, 'rb')
|
||||
self.fileno = self.file.fileno()
|
||||
|
||||
def tearDown(self):
|
||||
self.file.close()
|
||||
self.client.close()
|
||||
if self.server.running:
|
||||
self.server.stop()
|
||||
|
||||
def sendfile_wrapper(self, sock, file, offset, nbytes, headers=[], trailers=[]):
|
||||
"""A higher level wrapper representing how an application is
|
||||
supposed to use sendfile().
|
||||
"""
|
||||
while 1:
|
||||
try:
|
||||
if self.SUPPORT_HEADERS_TRAILERS:
|
||||
return os.sendfile(sock, file, offset, nbytes, headers,
|
||||
trailers)
|
||||
else:
|
||||
return os.sendfile(sock, file, offset, nbytes)
|
||||
except OSError as err:
|
||||
if err.errno == errno.ECONNRESET:
|
||||
# disconnected
|
||||
raise
|
||||
elif err.errno in (errno.EAGAIN, errno.EBUSY):
|
||||
# we have to retry send data
|
||||
continue
|
||||
else:
|
||||
raise
|
||||
|
||||
def test_send_whole_file(self):
|
||||
# normal send
|
||||
total_sent = 0
|
||||
offset = 0
|
||||
nbytes = 4096
|
||||
while 1:
|
||||
sent = self.sendfile_wrapper(self.sockno, self.fileno, offset, nbytes)
|
||||
if sent == 0:
|
||||
break
|
||||
offset += sent
|
||||
total_sent += sent
|
||||
self.assertTrue(sent <= nbytes)
|
||||
self.assertEqual(offset, total_sent)
|
||||
|
||||
self.assertEqual(total_sent, len(self.DATA))
|
||||
self.client.close()
|
||||
self.server.wait()
|
||||
data = self.server.handler_instance.get_data()
|
||||
self.assertEqual(hash(data), hash(self.DATA))
|
||||
|
||||
def test_send_at_certain_offset(self):
|
||||
# start sending a file at a certain offset
|
||||
total_sent = 0
|
||||
offset = len(self.DATA) / 2
|
||||
nbytes = 4096
|
||||
while 1:
|
||||
sent = self.sendfile_wrapper(self.sockno, self.fileno, offset, nbytes)
|
||||
if sent == 0:
|
||||
break
|
||||
offset += sent
|
||||
total_sent += sent
|
||||
self.assertTrue(sent <= nbytes)
|
||||
|
||||
self.client.close()
|
||||
self.server.wait()
|
||||
data = self.server.handler_instance.get_data()
|
||||
expected = self.DATA[int(len(self.DATA) / 2):]
|
||||
self.assertEqual(total_sent, len(expected))
|
||||
self.assertEqual(hash(data), hash(expected))
|
||||
|
||||
def test_offset_overflow(self):
|
||||
# specify an offset > file size
|
||||
offset = len(self.DATA) + 4096
|
||||
sent = os.sendfile(self.sockno, self.fileno, offset, 4096)
|
||||
self.assertEqual(sent, 0)
|
||||
self.client.close()
|
||||
self.server.wait()
|
||||
data = self.server.handler_instance.get_data()
|
||||
self.assertEqual(data, b'')
|
||||
|
||||
def test_invalid_offset(self):
|
||||
with self.assertRaises(OSError) as cm:
|
||||
os.sendfile(self.sockno, self.fileno, -1, 4096)
|
||||
self.assertEqual(cm.exception.errno, errno.EINVAL)
|
||||
|
||||
# --- headers / trailers tests
|
||||
|
||||
if SUPPORT_HEADERS_TRAILERS:
|
||||
|
||||
def test_headers(self):
|
||||
total_sent = 0
|
||||
sent = os.sendfile(self.sockno, self.fileno, 0, 4096,
|
||||
headers=[b"x" * 512])
|
||||
total_sent += sent
|
||||
offset = 4096
|
||||
nbytes = 4096
|
||||
while 1:
|
||||
sent = self.sendfile_wrapper(self.sockno, self.fileno,
|
||||
offset, nbytes)
|
||||
if sent == 0:
|
||||
break
|
||||
total_sent += sent
|
||||
offset += sent
|
||||
|
||||
expected_data = b"x" * 512 + self.DATA
|
||||
self.assertEqual(total_sent, len(expected_data))
|
||||
self.client.close()
|
||||
self.server.wait()
|
||||
data = self.server.handler_instance.get_data()
|
||||
self.assertEqual(hash(data), hash(expected_data))
|
||||
|
||||
def test_trailers(self):
|
||||
TESTFN2 = support.TESTFN + "2"
|
||||
f = open(TESTFN2, 'wb')
|
||||
f.write(b"abcde")
|
||||
f.close()
|
||||
f = open(TESTFN2, 'rb')
|
||||
try:
|
||||
os.sendfile(self.sockno, f.fileno(), 0, 4096, trailers=[b"12345"])
|
||||
self.client.close()
|
||||
self.server.wait()
|
||||
data = self.server.handler_instance.get_data()
|
||||
self.assertEqual(data, b"abcde12345")
|
||||
finally:
|
||||
os.remove(TESTFN2)
|
||||
|
||||
if hasattr(os, "SF_NODISKIO"):
|
||||
def test_flags(self):
|
||||
try:
|
||||
os.sendfile(self.sockno, self.fileno, 0, 4096,
|
||||
flags=os.SF_NODISKIO)
|
||||
except OSError as err:
|
||||
if err.errno not in (errno.EBUSY, errno.EAGAIN):
|
||||
raise
|
||||
|
||||
|
||||
def test_main():
|
||||
support.run_unittest(
|
||||
FileTests,
|
||||
|
|
@ -1281,6 +1533,7 @@ def test_main():
|
|||
PidTests,
|
||||
LoginTests,
|
||||
LinkTests,
|
||||
TestSendfile,
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue