<p>资源管理器具有共享删除/重命名访问权限的目录的打开句柄。这允许<code>rmdir</code>成功,而正常情况下,open不会共享删除/重命名访问,并且<code>rmdir</code>会因共享冲突而失败(32)。但是,即使<code>rmdir</code>成功,在资源管理器关闭其句柄之前,目录实际上不会解除链接。它正在监视目录的更改,因此会收到目录已被删除的通知,但即使它立即关闭其句柄,脚本的<code>os.mkdir</code>调用也会出现竞争条件</p>
<p>您应该在循环中重试<code>os.mkdir</code>,超时时间会增加。您还需要一个<code>onerror</code>处理程序来处理<code>shutil.rmtree</code>试图删除的目录不是空的,因为它包含“已删除”的文件或目录</p>
<p>例如:</p>
<pre><code>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
</code></pre>
<hr/>
<p><strong>讨论</strong></p>
<p>在常见情况下,可以“避免”此问题,因为现有打开不共享删除/重命名访问权限。在这种情况下,尝试删除文件或目录失败,共享冲突(winerror 32)。例如,如果某个目录作为进程的工作目录打开,则它不共享删除/重命名访问权限。对于常规文件,大多数程序只共享读/执行和写/附加访问</p>
<p>临时文件通常使用“删除/重命名访问共享”打开,尤其是使用“删除/重命名”访问打开临时文件时(例如,使用“关闭时删除”标志打开)。这是导致“已删除”文件仍然链接但无法访问的最常见原因。另一种情况是打开目录以监视更改(例如,请参见<a href="https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw" rel="noreferrer">^{<cd8>}</a>)。通常,此打开将共享删除/重命名访问权限,这是本问题中Explorer的情况</p>
<hr/>
<p>对于Unix开发人员来说,声明文件被删除而没有取消链接可能听起来很奇怪(至少可以这么说)。在Windows中,删除文件(或目录)只是在其文件控制块(FCB)上设置删除配置。当文件系统清除文件的最后一个内核文件对象引用时,设置了delete处置集的文件将自动取消链接。文件对象通常由<code>CreateFileW</code>创建,它返回对象的句柄。当文件对象的最后一个句柄关闭时,会触发文件对象的清理。由于子进程中的句柄继承或显式<code>DuplicateHandle</code>调用,文件对象可能存在多个句柄引用</p>
<p>重申一下,一个文件或目录可能被多个内核文件对象引用,每个内核文件对象可能被多个句柄引用。通常,使用经典的Windows删除语义,在文件解除链接之前,必须关闭所有句柄。此外,设置delete处置并不一定是最终的。如果任何打开的句柄具有删除/重命名访问权限,则实际上可以通过清除删除处置(例如,请参见<a href="https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-setfileinformationbyhandle" rel="noreferrer">^{<cd11>}</a>:<code>FileDispositionInfo</code>)来恢复对文件的访问权限</p>
<p>在Windows 10中,内核还支持POSIX delete语义,即一旦删除句柄关闭,文件或目录就会立即取消链接(请参阅NTAPI<a href="https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/ntddk/ns-ntddk-_file_disposition_information_ex" rel="noreferrer">^{<cd13>}</a>的详细信息)。NTFS已更新为支持POSIX删除语义。最近,WINAPI<code>DeleteFileW</code>(即Python<code>os.remove</code>)已切换到在文件系统支持的情况下使用它,但<code>RemoveDirectoryW</code>(即Python<code>os.rmdir</code>)仍限于经典的Windows删除</p>
<p>对于NTFS来说,实现POSIX语义相对容易。它只需设置删除配置并将文件重命名为NTFS保留目录“\$Extend\$Deleted”,其名称基于其文件ID。实际上,该文件似乎已取消链接,同时继续允许现有文件对象访问该文件。与传统删除相比,一个显著的区别是原始名称丢失,因此具有删除/重命名访问权限的现有句柄无法取消删除处置</p>