mirror of
https://github.com/python/cpython.git
synced 2025-08-24 02:35:59 +00:00
bpo-35332: Handle os.close() errors in shutil.rmtree() (GH-23766)
* Ignore os.close() errors when ignore_errors is True. * Pass os.close() errors to the error handler if specified. * os.close no longer retried after error. Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
This commit is contained in:
parent
de6bca9564
commit
11d88a178b
3 changed files with 56 additions and 2 deletions
|
@ -687,7 +687,12 @@ def _rmtree_safe_fd(topfd, path, onexc):
|
||||||
_rmtree_safe_fd(dirfd, fullname, onexc)
|
_rmtree_safe_fd(dirfd, fullname, onexc)
|
||||||
try:
|
try:
|
||||||
os.close(dirfd)
|
os.close(dirfd)
|
||||||
|
except OSError as err:
|
||||||
|
# close() should not be retried after an error.
|
||||||
dirfd_closed = True
|
dirfd_closed = True
|
||||||
|
onexc(os.close, fullname, err)
|
||||||
|
dirfd_closed = True
|
||||||
|
try:
|
||||||
os.rmdir(entry.name, dir_fd=topfd)
|
os.rmdir(entry.name, dir_fd=topfd)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
continue
|
continue
|
||||||
|
@ -704,7 +709,10 @@ def _rmtree_safe_fd(topfd, path, onexc):
|
||||||
onexc(os.path.islink, fullname, err)
|
onexc(os.path.islink, fullname, err)
|
||||||
finally:
|
finally:
|
||||||
if not dirfd_closed:
|
if not dirfd_closed:
|
||||||
os.close(dirfd)
|
try:
|
||||||
|
os.close(dirfd)
|
||||||
|
except OSError as err:
|
||||||
|
onexc(os.close, fullname, err)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
os.unlink(entry.name, dir_fd=topfd)
|
os.unlink(entry.name, dir_fd=topfd)
|
||||||
|
@ -782,7 +790,12 @@ def rmtree(path, ignore_errors=False, onerror=None, *, onexc=None, dir_fd=None):
|
||||||
_rmtree_safe_fd(fd, path, onexc)
|
_rmtree_safe_fd(fd, path, onexc)
|
||||||
try:
|
try:
|
||||||
os.close(fd)
|
os.close(fd)
|
||||||
|
except OSError as err:
|
||||||
|
# close() should not be retried after an error.
|
||||||
fd_closed = True
|
fd_closed = True
|
||||||
|
onexc(os.close, path, err)
|
||||||
|
fd_closed = True
|
||||||
|
try:
|
||||||
os.rmdir(path, dir_fd=dir_fd)
|
os.rmdir(path, dir_fd=dir_fd)
|
||||||
except OSError as err:
|
except OSError as err:
|
||||||
onexc(os.rmdir, path, err)
|
onexc(os.rmdir, path, err)
|
||||||
|
@ -794,7 +807,10 @@ def rmtree(path, ignore_errors=False, onerror=None, *, onexc=None, dir_fd=None):
|
||||||
onexc(os.path.islink, path, err)
|
onexc(os.path.islink, path, err)
|
||||||
finally:
|
finally:
|
||||||
if not fd_closed:
|
if not fd_closed:
|
||||||
os.close(fd)
|
try:
|
||||||
|
os.close(fd)
|
||||||
|
except OSError as err:
|
||||||
|
onexc(os.close, path, err)
|
||||||
else:
|
else:
|
||||||
if dir_fd is not None:
|
if dir_fd is not None:
|
||||||
raise NotImplementedError("dir_fd unavailable on this platform")
|
raise NotImplementedError("dir_fd unavailable on this platform")
|
||||||
|
|
|
@ -576,6 +576,41 @@ class TestRmTree(BaseTest, unittest.TestCase):
|
||||||
self.assertFalse(shutil._use_fd_functions)
|
self.assertFalse(shutil._use_fd_functions)
|
||||||
self.assertFalse(shutil.rmtree.avoids_symlink_attacks)
|
self.assertFalse(shutil.rmtree.avoids_symlink_attacks)
|
||||||
|
|
||||||
|
@unittest.skipUnless(shutil._use_fd_functions, "requires safe rmtree")
|
||||||
|
def test_rmtree_fails_on_close(self):
|
||||||
|
# Test that the error handler is called for failed os.close() and that
|
||||||
|
# os.close() is only called once for a file descriptor.
|
||||||
|
tmp = self.mkdtemp()
|
||||||
|
dir1 = os.path.join(tmp, 'dir1')
|
||||||
|
os.mkdir(dir1)
|
||||||
|
dir2 = os.path.join(dir1, 'dir2')
|
||||||
|
os.mkdir(dir2)
|
||||||
|
def close(fd):
|
||||||
|
orig_close(fd)
|
||||||
|
nonlocal close_count
|
||||||
|
close_count += 1
|
||||||
|
raise OSError
|
||||||
|
|
||||||
|
close_count = 0
|
||||||
|
with support.swap_attr(os, 'close', close) as orig_close:
|
||||||
|
with self.assertRaises(OSError):
|
||||||
|
shutil.rmtree(dir1)
|
||||||
|
self.assertTrue(os.path.isdir(dir2))
|
||||||
|
self.assertEqual(close_count, 2)
|
||||||
|
|
||||||
|
close_count = 0
|
||||||
|
errors = []
|
||||||
|
def onexc(*args):
|
||||||
|
errors.append(args)
|
||||||
|
with support.swap_attr(os, 'close', close) as orig_close:
|
||||||
|
shutil.rmtree(dir1, onexc=onexc)
|
||||||
|
self.assertEqual(len(errors), 2)
|
||||||
|
self.assertIs(errors[0][0], close)
|
||||||
|
self.assertEqual(errors[0][1], dir2)
|
||||||
|
self.assertIs(errors[1][0], close)
|
||||||
|
self.assertEqual(errors[1][1], dir1)
|
||||||
|
self.assertEqual(close_count, 2)
|
||||||
|
|
||||||
@unittest.skipUnless(shutil._use_fd_functions, "dir_fd is not supported")
|
@unittest.skipUnless(shutil._use_fd_functions, "dir_fd is not supported")
|
||||||
def test_rmtree_with_dir_fd(self):
|
def test_rmtree_with_dir_fd(self):
|
||||||
tmp_dir = self.mkdtemp()
|
tmp_dir = self.mkdtemp()
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
The :func:`shutil.rmtree` function now ignores errors when calling
|
||||||
|
:func:`os.close` when *ignore_errors* is ``True``, and
|
||||||
|
:func:`os.close` no longer retried after error.
|
Loading…
Add table
Add a link
Reference in a new issue