diff --git a/Doc/library/shutil.rst b/Doc/library/shutil.rst --- a/Doc/library/shutil.rst +++ b/Doc/library/shutil.rst @@ -236,25 +236,26 @@ Directory and files operations .. versionchanged:: 3.2 Added the *copy_function* argument to be able to provide a custom copy function. Added the *ignore_dangling_symlinks* argument to silent dangling symlinks errors when *symlinks* is false. -.. function:: rmtree(path, ignore_errors=False, onerror=None) +.. function:: rmtree(path, ignore_errors=False, onerror=None, wait=False) .. index:: single: directory; deleting Delete an entire directory tree; *path* must point to a directory (but not a symbolic link to a directory). If *ignore_errors* is true, errors resulting from failed removals will be ignored; if false or omitted, such errors are handled by calling a handler specified by *onerror* or, if that is omitted, - they raise an exception. + they raise an exception. If *wait* is true, the function blocks until the + path is deleted. .. note:: On platforms that support the necessary fd-based functions a symlink attack resistant version of :func:`rmtree` is used by default. On other platforms, the :func:`rmtree` implementation is susceptible to a symlink attack: given proper timing and circumstances, attackers can manipulate symlinks on the filesystem to delete files they wouldn't be able to access diff --git a/Lib/shutil.py b/Lib/shutil.py --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -7,16 +7,17 @@ XXX The functions here don't copy the re import os import sys import stat from os.path import abspath import fnmatch import collections import errno import tarfile +from time import sleep try: import bz2 del bz2 _BZ2_SUPPORTED = True except ImportError: _BZ2_SUPPORTED = False @@ -338,17 +339,17 @@ def copytree(src, dst, symlinks=False, i # Copying file access times may fail on Windows if why.winerror is None: errors.append((src, dst, str(why))) if errors: raise Error(errors) return dst # version vulnerable to race conditions -def _rmtree_unsafe(path, onerror): +def _rmtree_unsafe(path, onerror, wait): try: if os.path.islink(path): # symlinks to directories are forbidden, see bug #1669 raise OSError("Cannot call rmtree on a symbolic link") except OSError: onerror(os.path.islink, path, sys.exc_info()) # can't continue even if onerror hook returns return @@ -359,24 +360,26 @@ def _rmtree_unsafe(path, onerror): onerror(os.listdir, path, sys.exc_info()) for name in names: fullname = os.path.join(path, name) try: mode = os.lstat(fullname).st_mode except OSError: mode = 0 if stat.S_ISDIR(mode): - _rmtree_unsafe(fullname, onerror) + _rmtree_unsafe(fullname, onerror, wait) else: try: os.unlink(fullname) except OSError: onerror(os.unlink, fullname, sys.exc_info()) try: os.rmdir(path) + while wait and os.path.exists(path): + sleep(0.01) except OSError: onerror(os.rmdir, path, sys.exc_info()) # Version using fd-based APIs to protect against races def _rmtree_safe_fd(topfd, path, onerror): names = [] try: names = os.listdir(topfd) @@ -420,25 +423,26 @@ def _rmtree_safe_fd(topfd, path, onerror except OSError: onerror(os.unlink, fullname, sys.exc_info()) _use_fd_functions = ({os.open, os.stat, os.unlink, os.rmdir} <= os.supports_dir_fd and os.listdir in os.supports_fd and os.stat in os.supports_follow_symlinks) -def rmtree(path, ignore_errors=False, onerror=None): +def rmtree(path, ignore_errors=False, onerror=None, wait=False): """Recursively delete a directory tree. If ignore_errors is set, errors are ignored; otherwise, if onerror is set, it is called to handle the error with arguments (func, path, exc_info) where func is platform and implementation dependent; path is the argument to that function that caused it to fail; and exc_info is a tuple returned by sys.exc_info(). If ignore_errors - is false and onerror is None, an exception is raised. + is false and onerror is None, an exception is raised. If wait is set, + the function blocks indefinitely until the path is deleted. """ if ignore_errors: def onerror(*args): pass elif onerror is None: def onerror(*args): raise @@ -458,28 +462,30 @@ def rmtree(path, ignore_errors=False, on except Exception: onerror(os.lstat, path, sys.exc_info()) return try: if os.path.samestat(orig_st, os.fstat(fd)): _rmtree_safe_fd(fd, path, onerror) try: os.rmdir(path) + while wait and os.path.exists(path): + sleep(0.01) except OSError: onerror(os.rmdir, path, sys.exc_info()) else: try: # symlinks to directories are forbidden, see bug #1669 raise OSError("Cannot call rmtree on a symbolic link") except OSError: onerror(os.path.islink, path, sys.exc_info()) finally: os.close(fd) else: - return _rmtree_unsafe(path, onerror) + return _rmtree_unsafe(path, onerror, wait) # Allow introspection of whether or not the hardening against symlink # attacks is supported on the current platform rmtree.avoids_symlink_attacks = _use_fd_functions def _basename(path): # A basename() variant which first strips the trailing slash, if present. # Thus we always get the last component of the path, even for directories. diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -16,16 +16,18 @@ from os.path import splitdrive from distutils.spawn import find_executable, spawn from shutil import (_make_tarball, _make_zipfile, make_archive, register_archive_format, unregister_archive_format, get_archive_formats, Error, unpack_archive, register_unpack_format, RegistryError, unregister_unpack_format, get_unpack_formats, SameFileError) import tarfile +thread = support.import_module('_thread') +from time import sleep import warnings from test import support from test.support import TESTFN, check_warnings, captured_stdout, requires_zlib try: import bz2 BZ2_SUPPORTED = True @@ -612,16 +614,30 @@ class TestShutil(unittest.TestCase): def test_rmtree_dont_delete_file(self): # When called on a file instead of a directory, don't delete it. handle, path = tempfile.mkstemp() os.close(handle) self.assertRaises(NotADirectoryError, shutil.rmtree, path) os.remove(path) + def test_rmtree_with_wait(self): + # When called wait set, blocks indefinitely until. + tmp_dir = tempfile.mkdtemp() + src = os.path.join(tmp_dir, 'src') + write_file(src, 'foo') + + def task(): + with open(src, 'r') as f: + sleep(0.1) + + thread.start_new_thread(task, ()) + shutil.rmtree(tmp_dir, wait=True) + self.assertFalse(os.path.exists(tmp_dir)) + def test_copytree_simple(self): src_dir = tempfile.mkdtemp() dst_dir = os.path.join(tempfile.mkdtemp(), 'destination') self.addCleanup(shutil.rmtree, src_dir) self.addCleanup(shutil.rmtree, os.path.dirname(dst_dir)) write_file((src_dir, 'test.txt'), '123') os.mkdir(os.path.join(src_dir, 'test_dir')) write_file((src_dir, 'test_dir', 'test.txt'), '456')