Skip to content

Commit ad3a396

Browse files
miss-islingtongrantlouishermanvstinner
authored
[3.13] gh-150107: Fix asyncio sendfile fallback ignoring non-zero offset (GH-150270) (#150571)
gh-150107: Fix asyncio sendfile fallback ignoring non-zero offset (GH-150270) (cherry picked from commit c72d5ea) Co-authored-by: Grant Herman <grantlouisherman041@gmail.com> Co-authored-by: Victor Stinner <vstinner@python.org>
1 parent ddd50ab commit ad3a396

6 files changed

Lines changed: 73 additions & 14 deletions

File tree

Lib/asyncio/base_events.py

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

968968
async def _sock_sendfile_fallback(self, sock, file, offset, count):
969-
if offset:
969+
if hasattr(file, 'seek'):
970970
file.seek(offset)
971971
blocksize = (
972972
min(count, constants.SENDFILE_FALLBACK_READBUFFER_SIZE)
@@ -1281,7 +1281,6 @@ async def sendfile(self, transport, file, offset=0, count=None,
12811281
raise RuntimeError(
12821282
f"fallback is disabled and native sendfile is not "
12831283
f"supported for transport {transport!r}")
1284-
12851284
return await self._sendfile_fallback(transport, file,
12861285
offset, count)
12871286

@@ -1290,7 +1289,7 @@ async def _sendfile_native(self, transp, file, offset, count):
12901289
"sendfile syscall is not supported")
12911290

12921291
async def _sendfile_fallback(self, transp, file, offset, count):
1293-
if offset:
1292+
if hasattr(file, 'seek'):
12941293
file.seek(offset)
12951294
blocksize = min(count, 16384) if count else 16384
12961295
buf = bytearray(blocksize)

Lib/asyncio/proactor_events.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -756,8 +756,7 @@ async def _sock_sendfile_native(self, sock, file, offset, count):
756756
offset += blocksize
757757
total_sent += blocksize
758758
finally:
759-
if total_sent > 0:
760-
file.seek(offset)
759+
file.seek(offset)
761760

762761
async def _sendfile_native(self, transp, file, offset, count):
763762
resume_reading = transp.is_reading()

Lib/asyncio/unix_events.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -395,12 +395,12 @@ def _sock_sendfile_native_impl(self, fut, registered_fd, sock, fileno,
395395
# order to simplify the common case.
396396
self.remove_writer(registered_fd)
397397
if fut.cancelled():
398-
self._sock_sendfile_update_filepos(fileno, offset, total_sent)
398+
self._sock_sendfile_update_filepos(fileno, offset)
399399
return
400400
if count:
401401
blocksize = count - total_sent
402402
if blocksize <= 0:
403-
self._sock_sendfile_update_filepos(fileno, offset, total_sent)
403+
self._sock_sendfile_update_filepos(fileno, offset)
404404
fut.set_result(total_sent)
405405
return
406406

@@ -434,20 +434,20 @@ def _sock_sendfile_native_impl(self, fut, registered_fd, sock, fileno,
434434
# plain send().
435435
err = exceptions.SendfileNotAvailableError(
436436
"os.sendfile call failed")
437-
self._sock_sendfile_update_filepos(fileno, offset, total_sent)
437+
self._sock_sendfile_update_filepos(fileno, offset)
438438
fut.set_exception(err)
439439
else:
440-
self._sock_sendfile_update_filepos(fileno, offset, total_sent)
440+
self._sock_sendfile_update_filepos(fileno, offset)
441441
fut.set_exception(exc)
442442
except (SystemExit, KeyboardInterrupt):
443443
raise
444444
except BaseException as exc:
445-
self._sock_sendfile_update_filepos(fileno, offset, total_sent)
445+
self._sock_sendfile_update_filepos(fileno, offset)
446446
fut.set_exception(exc)
447447
else:
448448
if sent == 0:
449449
# EOF
450-
self._sock_sendfile_update_filepos(fileno, offset, total_sent)
450+
self._sock_sendfile_update_filepos(fileno, offset)
451451
fut.set_result(total_sent)
452452
else:
453453
offset += sent
@@ -458,9 +458,9 @@ def _sock_sendfile_native_impl(self, fut, registered_fd, sock, fileno,
458458
fd, sock, fileno,
459459
offset, count, blocksize, total_sent)
460460

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

465465
def _sock_add_cancellation_callback(self, fut, sock):
466466
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: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,61 @@ 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+
def check_sendfile_offset(self, offset, fallback):
262+
srv_proto, cli_proto = self.prepare_sendfile()
263+
self.file.seek(123)
264+
coro = self.loop.sendfile(cli_proto.transport, self.file, offset, fallback=fallback)
265+
try:
266+
ret = self.run_loop(coro)
267+
except asyncio.SendfileNotAvailableError:
268+
if fallback:
269+
raise
270+
cli_proto.transport.close()
271+
self.run_loop(srv_proto.done)
272+
return
273+
cli_proto.transport.close()
274+
self.run_loop(srv_proto.done)
275+
self.assertEqual(ret, len(self.DATA) - offset)
276+
self.assertEqual(srv_proto.nbytes, len(self.DATA) - offset)
277+
self.assertEqual(srv_proto.data, self.DATA[offset:])
278+
self.assertEqual(self.file.tell(), len(self.DATA))
279+
280+
def test_sendfile_offset(self):
281+
for offset in (0, len(self.DATA) // 2, len(self.DATA)):
282+
for fallback in (False, True):
283+
with self.subTest(offset=offset, fallback=fallback):
284+
self.check_sendfile_offset(offset, fallback)
285+
231286
def test_sock_sendfile_mix_with_regular_send(self):
232287
buf = b"mix_regular_send" * (4 * 1024) # 64 KiB
233288
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)