在 Windows 上成功使用 shutdown.rmtree 后,os.mkdir 可能会失败并出现 PermissionError

2023-12-21

考虑以下用于清理目录的 python 函数:

def cleanDir(path):
  shutil.rmtree(path)
  os.mkdir(path)

在 Windows 上(实际在 Windows7 和 Windows10 上使用 python 2.7.10 和 3.4.4 进行测试),当使用 Windows 资源管理器同时导航到相应目录时(或者仅在左侧树窗格中导航到父文件夹时),可能会引发以下异常:

Traceback (most recent call last):
  ...
  File "cleanDir.py", line ..., in cleanDir
    os.mkdir(path)
PermissionError: [WinError 5] Access is denied: 'testFolder'

该问题已在此报告issue https://stackoverflow.com/questions/49809471。但没有进一步分析,并且给出的使用 sleep 的解决方案并不令人满意。根据下面 Eryk 的评论,当前的 python 版本(即 python 3.8)也会出现相同的行为。

注意shutil.rmtree无一例外地返回。但尝试立即再次创建该目录可能会失败。 (重试大多数情况下都会成功,请参阅下面的完整测试代码。)并请注意,您需要在 Windows 资源管理器中的测试文件夹的左侧和右侧单击,以强制解决问题。

问题似乎出在 Windows 文件系统 API 函数中(而不是在 Python 中)osmodule):当 Windows 资源管理器具有相应文件夹的句柄时,已删除的文件夹似乎不会立即“转发”到所有功能。

import os, shutil
import time

def populateFolder(path):
  if os.path.exists(path):
    with open(os.path.join(path,'somefile.txt'), 'w') as f:
      f.write('test')
  #subfolderpath = os.path.join(path,'subfolder')
  #os.mkdir(subfolderpath)
  #with open(os.path.join(subfolderpath,'anotherfile.txt'), 'w') as f2:
  #  f2.write('test')

def cleanDir(path):
  shutil.rmtree(path)
  os.mkdir(path)


def cleanDir_safe(path):
  shutil.rmtree(path)

  try:
    #time.sleep(0.005) # makes first try of os.mkdir successful
    os.mkdir(path)
  except Exception as e:
    print('os.mkdir failed: %s' % e)
    time.sleep(0.01)
    os.mkdir(path)

  assert os.path.exists(path)


FOLDER_PATH = 'testFolder'
if os.path.exists(FOLDER_PATH):
  cleanDir(FOLDER_PATH)
else:
  os.mkdir(FOLDER_PATH)

loopCnt = 0
while True:
  populateFolder(FOLDER_PATH)
  #cleanDir(FOLDER_PATH)
  cleanDir_safe(FOLDER_PATH)
  time.sleep(0.01)
  loopCnt += 1
  if loopCnt % 100 == 0:
    print(loopCnt)

资源管理器具有共享删除/重命名访问权限的目录的开放句柄。这允许rmdir成功,而通常打开不会共享删除/重命名访问权限,并且rmdir会因共享违规而失败 (32)。然而,尽管rmdir成功后,该目录实际上不会取消链接,直到资源管理器关闭其句柄。它正在监视目录的更改,因此它会收到目录已被删除的通知,但即使它立即关闭其句柄,也存在与脚本的竞争条件os.mkdir call.

你应该重试os.mkdir循环中,超时时间不断增加。您还需要一个onerror处理程序shutil.rmtree它处理尝试删除不为空的目录,因为它包含“已删除”的文件或目录。

例如:

import os
import time
import errno
import shutil

def onerror(function, path, exc_info):
    # Handle ENOTEMPTY for rmdir
    if (function is os.rmdir
          and issubclass(exc_info[0], OSError)
          and exc_info[1].errno == errno.ENOTEMPTY):
        timeout = 0.001
        while timeout < 2:
            if not os.listdir(path):
                return os.rmdir(path)
            time.sleep(timeout)
            timeout *= 2
    raise

def clean_dir_safe(path):
    shutil.rmtree(path, onerror=onerror)
    # rmtree didn't fail, but path may still be linked if there is or was
    # a handle that shares delete access. Assume the owner of the handle
    # is watching for changes and will close it ASAP. So retry creating
    # the directory by using a loop with an increasing timeout.
    timeout = 0.001
    while True:
        try:
            return os.mkdir(path)
        except PermissionError as e:
            # Getting access denied (5) when trying to create a file or
            # directory means either the caller lacks access to the
            # parent directory or that a file or directory with that
            # name exists but is in the deleted state. Handle both cases
            # the same way. Otherwise, re-raise the exception for other
            # permission errors, such as a sharing violation (32).
            if e.winerror != 5 or timeout >= 2:
                raise
            time.sleep(timeout)
            timeout *= 2

讨论

在常见情况下,此问题是“避免”的,因为现有打开不共享删除/重命名访问权限。在这种情况下,尝试删除文件或目录会因共享冲突而失败(winerror 32)。例如,如果一个目录作为进程的工作目录打开,则它不共享删除/重命名访问权限。对于常规文件,大多数程序仅共享读/执行和写/追加访问权限。

临时文件通常通过删除/重命名访问共享打开,尤其是通过删除/重命名访问打开时(例如,使用关闭时删除标志打开)。这是导致“已删除”文件仍然链接但无法访问的最常见原因。另一种情况是打开一个目录来观察更改(例如,参见ReadDirectoryChangesW https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw)。通常,此打开将共享删除/重命名访问权限,这就是本问题中资源管理器的情况。


对于 Unix 开发人员来说,声明文件在不取消链接的情况下被删除可能听起来很奇怪(至少可以这么说)。在Windows中,删除文件(或目录)只是在其文件控制块(FCB)上设置删除配置。当文件系统清理文件的最后一个内核文件对象引用时,设置了删除处置的文件会自动取消链接。文件对象通常由以下方式创建CreateFileW,它返回对象的句柄。当文件对象的最后一个句柄关闭时,会触发文件对象的清理。由于子进程中的句柄继承或显式的句柄继承,可能存在文件对象的多个句柄引用DuplicateHandle calls.

重申一下,一个文件或目录可以被多个内核文件对象引用,其中每个文件或目录可以被多个句柄引用。通常,使用经典的 Windows 删除语义,在文件取消链接之前必须关闭所有句柄。此外,设置删除处置不一定是最终的。如果任何打开的句柄具有删除/重命名访问权限,则实际上可以通过清除删除配置来恢复对文件的访问权限(例如,请参阅SetFileInformationByHandle https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-setfileinformationbyhandle: FileDispositionInfo).

在 Windows 10 中,内核还支持 POSIX 删除语义,一旦删除句柄关闭,文件或目录就会立即取消链接(请参阅 NTAPI 的详细信息)FileDispositionInformationEx https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntddk/ns-ntddk-_file_disposition_information_ex)。 NTFS 已更新为支持 POSIX 删除语义。最近WINAPIDeleteFileW(即Pythonos.remove)如果文件系统支持它,则已切换为使用它,但是RemoveDirectoryW(即Pythonos.rmdir)仍然仅限于经典的 Windows 删除。

对于 NTFS 来说,实现 POSIX 语义相对容易。它只是设置删除配置,并将文件重命名为 NTFS 保留目录“\$Extend\$Deleted”,名称基于其文件 ID。实际上,文件似乎已取消链接,同时继续允许现有文件对象访问该文件。与经典删除相比,一个显着区别是原始名称丢失,因此具有删除/重命名访问权限的现有句柄无法取消设置删除配置。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

在 Windows 上成功使用 shutdown.rmtree 后,os.mkdir 可能会失败并出现 PermissionError 的相关文章

随机推荐