【问题标题】:os.mkdir can fail with PermissionError after successful shutil.rmtree on Windows在 Windows 上成功 shutil.rmtree 后,os.mkdir 可能会因 PermissionError 而失败
【发布时间】:2020-02-07 11:41:52
【问题描述】:

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

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

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

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

此问题已在此issue 中报告。但是没有进一步分析,使用睡眠的给定解决方案并不令人满意。根据 Eryk 的 cmets,在当前的 python 版本(即 python 3.8)中,同样的行为也可以预期。

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

问题似乎出在 Windows 文件系统 API 函数中(而不是在 Python os 模块中):当 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)

【问题讨论】:

  • @ErykSun 使用 python 2.7.10 和 python 3.4.4 测试
  • @ErykSun 我们已经明确观察到这种行为:shutil.rmtree 成功后,os.path.exists(path)(“读取访问”)始终为False。如果在shutil.rmtree 之后“快速”调用os.mkdir(path)(“写访问”)可能会失败。使用给定的测试代码,这应该很容易重现。
  • @ErykSun“已删除但仍链接的目录”:这种暂时不一致的状态不是 Windows 文件系统 API 中的错误吗?
  • @ErykSun 所以从 python 3.5.8 开始(例如也在 python 3.6 中)在shutil.rmdir(path) 之后直接调用os.mkdir(path) 是安全的吗?
  • @ErykSun 带有FindFirstFileW 的示例代码会很棒,因为我们无法立即将代码库升级到 python 3.5+

标签: python windows


【解决方案1】:

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

您应该在循环中重试os.mkdir,并增加超时时间。您还需要 shutil.rmtreeonerror 处理程序来处理尝试删除非空目录,因为它包含“已删除”的文件或目录。

例如:

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)。通常,此打开将共享删除/重命名访问权限,这就是本题中 Explorer 的情况。


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

重申一下,一个文件或目录可以被多个内核文件对象引用,每个内核文件对象都可以被多个句柄引用。通常,使用经典的 Windows 删除语义,必须在文件取消链接之前关闭所有句柄。此外,设置删除处置不一定是最终的。如果任何打开的句柄具有删除/重命名权限,它实际上可以用于通过清除删除处置来恢复对文件的访问(例如,参见SetFileInformationByHandle:FileDispositionInfo)。

在 Windows 10 中,内核还支持 POSIX 删除语义,一旦删除句柄关闭,文件或目录就会立即取消链接(请参阅 NTAPI FileDispositionInformationEx 的详细信息)。 NTFS 已更新以支持 POSIX 删除语义。如果文件系统支持,最近 WINAPI DeleteFileW(即 Python os.remove)已切换到使用它,但 RemoveDirectoryW(即 Python os.rmdir)仍然仅限于经典的 Windows 删除。

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

【讨论】:

    【解决方案2】:

    我重命名文件夹并删除“新”文件夹,然后创建(预期的)文件夹。

    ## Rename & Delete 'old' folder
    if os.path.exists(file_path):
        sTmp = "fldr_" + datetime.now().strftime("%Y%m%d%H%M%S") # new folder name
        os.rename(file_path, sTmp)  # rename folder
        shutil.rmtree(sTmp)  # delete folder
    
    ## Create new folder
    os.makedirs(file_path)  # make new folder
    

    【讨论】:

      猜你喜欢
      • 2011-02-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-05-09
      • 2021-08-21
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多