Skip to content

Commit 3b33313

Browse files
Fix asyncio sendfile fallback ignoring non-zero offset
Signed-off-by: grantlouisherman <grantlouisherman041@gmail.com>
1 parent f500e4e commit 3b33313

5 files changed

Lines changed: 66 additions & 12 deletions

File tree

Lib/asyncio/base_events.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -969,7 +969,7 @@ async def _sock_sendfile_native(self, sock, file, offset, count):
969969
f"and file {file!r} combination")
970970

971971
async def _sock_sendfile_fallback(self, sock, file, offset, count):
972-
if offset:
972+
if hasattr(file, 'seek'):
973973
file.seek(offset)
974974
blocksize = (
975975
min(count, constants.SENDFILE_FALLBACK_READBUFFER_SIZE)
@@ -1286,7 +1286,6 @@ async def sendfile(self, transport, file, offset=0, count=None,
12861286
raise RuntimeError(
12871287
f"fallback is disabled and native sendfile is not "
12881288
f"supported for transport {transport!r}")
1289-
12901289
return await self._sendfile_fallback(transport, file,
12911290
offset, count)
12921291

@@ -1295,7 +1294,7 @@ async def _sendfile_native(self, transp, file, offset, count):
12951294
"sendfile syscall is not supported")
12961295

12971296
async def _sendfile_fallback(self, transp, file, offset, count):
1298-
if offset:
1297+
if hasattr(file, 'seek'):
12991298
file.seek(offset)
13001299
blocksize = min(count, 16384) if count else 16384
13011300
buf = bytearray(blocksize)

Lib/asyncio/unix_events.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -385,12 +385,12 @@ def _sock_sendfile_native_impl(self, fut, registered_fd, sock, fileno,
385385
# order to simplify the common case.
386386
self.remove_writer(registered_fd)
387387
if fut.cancelled():
388-
self._sock_sendfile_update_filepos(fileno, offset, total_sent)
388+
self._sock_sendfile_update_filepos(fileno, offset)
389389
return
390390
if count:
391391
blocksize = count - total_sent
392392
if blocksize <= 0:
393-
self._sock_sendfile_update_filepos(fileno, offset, total_sent)
393+
self._sock_sendfile_update_filepos(fileno, offset)
394394
fut.set_result(total_sent)
395395
return
396396

@@ -424,20 +424,20 @@ def _sock_sendfile_native_impl(self, fut, registered_fd, sock, fileno,
424424
# plain send().
425425
err = exceptions.SendfileNotAvailableError(
426426
"os.sendfile call failed")
427-
self._sock_sendfile_update_filepos(fileno, offset, total_sent)
427+
self._sock_sendfile_update_filepos(fileno, offset)
428428
fut.set_exception(err)
429429
else:
430-
self._sock_sendfile_update_filepos(fileno, offset, total_sent)
430+
self._sock_sendfile_update_filepos(fileno, offset)
431431
fut.set_exception(exc)
432432
except (SystemExit, KeyboardInterrupt):
433433
raise
434434
except BaseException as exc:
435-
self._sock_sendfile_update_filepos(fileno, offset, total_sent)
435+
self._sock_sendfile_update_filepos(fileno, offset)
436436
fut.set_exception(exc)
437437
else:
438438
if sent == 0:
439439
# EOF
440-
self._sock_sendfile_update_filepos(fileno, offset, total_sent)
440+
self._sock_sendfile_update_filepos(fileno, offset)
441441
fut.set_result(total_sent)
442442
else:
443443
offset += sent
@@ -448,9 +448,9 @@ def _sock_sendfile_native_impl(self, fut, registered_fd, sock, fileno,
448448
fd, sock, fileno,
449449
offset, count, blocksize, total_sent)
450450

451-
def _sock_sendfile_update_filepos(self, fileno, offset, total_sent):
452-
if total_sent > 0:
453-
os.lseek(fileno, offset, os.SEEK_SET)
451+
def _sock_sendfile_update_filepos(self, fileno, offset):
452+
# After this helper runs, the source fd's lseek pointer is at offset."
453+
os.lseek(fileno, offset, os.SEEK_SET)
454454

455455
def _sock_add_cancellation_callback(self, fut, sock):
456456
def cb(fut):

Lib/asyncio/windows_events.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,9 @@ def sendfile(self, sock, file, offset, count):
610610
ov = _overlapped.Overlapped(NULL)
611611
offset_low = offset & 0xffff_ffff
612612
offset_high = (offset >> 32) & 0xffff_ffff
613+
# TransmitFile ignores OVERLAPPED.Offset for handles not opened with
614+
# FILE_FLAG_OVERLAPPED, so seek the CRT file pointer to match.
615+
file.seek(offset)
613616
ov.TransmitFile(sock.fileno(),
614617
msvcrt.get_osfhandle(file.fileno()),
615618
offset_low, offset_high,

Lib/test/test_asyncio/test_sendfile.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,55 @@ def test_sock_sendfile_zero_size(self):
228228
self.assertEqual(ret, 0)
229229
self.assertEqual(self.file.tell(), 0)
230230

231+
def check_sock_sendfile_offset(self, data, offset, force_fallback=False):
232+
sock, proto = self.prepare_socksendfile()
233+
with tempfile.TemporaryFile() as f:
234+
f.write(data)
235+
f.flush()
236+
self.assertEqual(f.tell(), len(data))
237+
238+
if force_fallback:
239+
async def _sock_sendfile_fail(sock, file, offset, count):
240+
raise asyncio.exceptions.SendfileNotAvailableError()
241+
with support.swap_attr(self.loop, '_sock_sendfile_native', _sock_sendfile_fail):
242+
ret = self.run_loop(self.loop.sock_sendfile(sock, f, offset, None))
243+
else:
244+
ret = self.run_loop(self.loop.sock_sendfile(sock, f, offset, None))
245+
246+
self.assertEqual(f.tell(), len(data))
247+
248+
sock.close()
249+
self.run_loop(proto.wait_closed())
250+
251+
self.assertEqual(ret, len(data) - offset)
252+
253+
254+
def test_sock_sendfile_offset(self):
255+
data = b'abcdef'
256+
for offset in (0, len(data) // 2, len(data)):
257+
for force_fallback in (False, True):
258+
with self.subTest(offset=offset, force_fallback=force_fallback):
259+
self.check_sock_sendfile_offset(data, offset, force_fallback)
260+
261+
262+
def check_sendfile_offset(self, offset, fallback):
263+
srv_proto, cli_proto = self.prepare_sendfile()
264+
self.file.seek(123)
265+
coro = self.loop.sendfile(cli_proto.transport, self.file, offset, fallback=fallback)
266+
ret = self.run_loop(coro)
267+
cli_proto.transport.close()
268+
self.run_loop(srv_proto.done)
269+
self.assertEqual(ret, len(self.DATA) - offset)
270+
self.assertEqual(srv_proto.nbytes, len(self.DATA) - offset)
271+
self.assertEqual(srv_proto.data, self.DATA[offset:])
272+
self.assertEqual(self.file.tell(), len(self.DATA))
273+
274+
def test_sendfile_offset(self):
275+
for offset in (0, len(self.DATA) // 2, len(self.DATA)):
276+
for fallback in (False, True):
277+
with self.subTest(offset=offset, fallback=fallback):
278+
self.check_sendfile_offset(offset, fallback)
279+
231280
def test_sock_sendfile_mix_with_regular_send(self):
232281
buf = b"mix_regular_send" * (4 * 1024) # 64 KiB
233282
sock, proto = self.prepare_socksendfile()
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:mod:`asyncio`: ``sendfile()`` and ``sock_sendfile()`` event loop methods
2+
now call ``file.seek(offset)`` if *file* has a ``seek()`` method,
3+
even if *offset* is ``0`` (default value).

0 commit comments

Comments
 (0)