【问题标题】:ReadDirectoryChangesW unable to detect and handle deletion of watched directoryReadDirectoryChangesW 无法检测和处理监视目录的删除
【发布时间】:2026-01-25 19:00:01
【问题描述】:

我尝试监视目录树的内容,其中包含大量文件(例如许多目录,每个目录有 9000 个文件)。

同步模式:

我首先尝试在阻塞模式(同步)下使用 ReadDirectoryChangesW,但是当我删除监视的目录时,我最终陷入了无法检测或退出的死锁。

#
# Monitors a directory for changes and pass the changes to the queue
#
def MonitorDirectory(self, out_queue):

    print("Monitoring instance \'{0}\' is watching directory: {1}".format(self.name, self.path))

    # File monitor
    FILE_LIST_DIRECTORY = 0x0001

    buffer = win32file.AllocateReadBuffer(1024 * 64)

    hDir = win32file.CreateFile(self.path,
                                FILE_LIST_DIRECTORY,
                                win32con.FILE_SHARE_READ | win32con.FILE_SHARE_WRITE | win32con.FILE_SHARE_DELETE,
                                None,
                                win32con.OPEN_EXISTING,
                                win32con.FILE_FLAG_BACKUP_SEMANTICS,
                                None)

    # Monitor directory for changes
    while not self._shutdown.is_set():

        # Create handle to directory if missing
        #if os.path.isdir(self.path):

        self.fh.write("ReOpen Exists {0}\n".format(os.path.isdir(self.path)))
        self.fh.flush()
        try:
            hDir = win32file.CreateFile(self.path,
                                FILE_LIST_DIRECTORY,
                                win32con.FILE_SHARE_READ | win32con.FILE_SHARE_WRITE | win32con.FILE_SHARE_DELETE,
                                None,
                                win32con.OPEN_EXISTING,
                                win32con.FILE_FLAG_BACKUP_SEMANTICS,
                                None)
        except:
            self.fh.write("Handle is dead\n")
            self.fh.flush()

        try:
            self.fh.write("{0}\n".format(newH))
            self.fh.flush()
        except:
            self.fh.write("Write failed\n")
            self.fh.flush()

        self.fh.write("Check Changes\n")
        self.fh.flush()

        results = win32file.ReadDirectoryChangesW(hDir,
                                                    1024 * 64,
                                                    True,
                                                    win32con.FILE_NOTIFY_CHANGE_FILE_NAME |
                                                    win32con.FILE_NOTIFY_CHANGE_DIR_NAME |
                                                    win32con.FILE_NOTIFY_CHANGE_ATTRIBUTES |
                                                    win32con.FILE_NOTIFY_CHANGE_SIZE |
                                                    win32con.FILE_NOTIFY_CHANGE_LAST_WRITE |
                                                    win32con.FILE_NOTIFY_CHANGE_SECURITY,
                                                    None,
                                                    None)

        # Add all changes to queue
        for action, file in results:

            self.fh.write("Action: {0} on {1}\n".format(action, file))

            out_queue.put((action, time.time(), os.path.join(self.path, file)))

        self.fh.flush()


        #else:


    # Done main loop
    print("Monitoring instance \'{0}\' has finished watching directory: {1}".format(self.name, self.path))

似乎没有办法避免在监视目录被删除时阻塞调用?

此外,由于该函数在线程中运行,因此在死锁时我无法从“主管”线程中将其杀死,该线程将监视父目录以在被监视目录上执行 DELETE 操作,我真的不喜欢这是一个好的解决方案,因为它涉及更多的代码。

A同步模式:

然后我尝试了重叠模式(异步),它不会在死锁中阻塞,但是当删除目录时,我无法检测到目录句柄何时变为无效。 WaitForSingleObject 调用只是时间到了,使用 os.path.isdir 检查目录是否存在并没有帮助,因为如果同时重新创建目录,它将不返回 False,但旧的目录句柄仍然无效,不会检测到新创建的同名目录的变化。

经过几天尝试各种方法后,我终于得到了这段代码,但是它不能完美地工作,因为它仍然没有检测到监视目录的删除,并且在快速大量删除文件时它也会丢失一些文件。同步模式没有的东西。

#
# Monitors a directory for changes and pass the changes to the queue
#
def MonitorDirectory(self, out_queue):

    print("Monitoring instance \'{0}\' is watching directory: {1}".format(self.name, self.path))

    # File monitor
    FILE_LIST_DIRECTORY = 0x0001

    overlapped          = pywintypes.OVERLAPPED()
    overlapped.hEvent   = win32event.CreateEvent(None, False, 0, None)

    buffer  = win32file.AllocateReadBuffer(1024 * 64)

    # Main loop to keep watching active
    while not self._shutdown.is_set():

        # Open directory
        try:
            hDir = win32file.CreateFile(self.path,
                                        FILE_LIST_DIRECTORY,
                                        win32con.FILE_SHARE_READ | win32con.FILE_SHARE_WRITE | win32con.FILE_SHARE_DELETE,
                                        None,
                                        win32con.OPEN_EXISTING,
                                        win32con.FILE_FLAG_BACKUP_SEMANTICS | win32con.FILE_FLAG_OVERLAPPED,
                                        None)

        except: 

            # Wait before retry
            time.sleep(1)

        else:

            # Monitor directory for changes
            while not self._shutdown.is_set():

                win32file.ReadDirectoryChangesW(hDir,
                                                buffer,
                                                True,
                                                win32con.FILE_NOTIFY_CHANGE_FILE_NAME |
                                                win32con.FILE_NOTIFY_CHANGE_DIR_NAME |
                                                win32con.FILE_NOTIFY_CHANGE_ATTRIBUTES |
                                                win32con.FILE_NOTIFY_CHANGE_SIZE |
                                                win32con.FILE_NOTIFY_CHANGE_LAST_WRITE |
                                                win32con.FILE_NOTIFY_CHANGE_SECURITY,
                                                overlapped,
                                                None)

                # Wait for the changes
                rc = win32event.WaitForSingleObject(overlapped.hEvent, 10000)

                if rc == win32event.WAIT_OBJECT_0:

                    try:
                        bytes_returned = win32file.GetOverlappedResult(hDir, overlapped, True)

                    except:
                        raise Exception("Error: handle invalid?")

                    else:

                        # Get the changes
                        for action, file in win32file.FILE_NOTIFY_INFORMATION(buffer, bytes_returned):                        
                            out_queue.put((action, time.time(), os.path.join(self.path, file)))

                elif rc == win32event.WAIT_TIMEOUT:
                    print("Monitoring instance \'{0}\': Timeout, no actions")

                else:
                    raise Exception("Error?! RC = {0}".format(rc))

    # Done main loop
    print("Monitoring instance \'{0}\' has finished watching directory: {1}".format(self.name, self.path))

有没有办法处理检测到被监视目录的删除,而不是仅仅删除 win32con.FILE_SHARE_DELETE 标志?

【问题讨论】:

    标签: python multithreading python-3.x pywin32 readdirectorychangesw


    【解决方案1】:

    注意事项

    现在,在 FILE_SHARE_DELETE 上说几句话(可以在 [MS.Docs]: CreateFileW function 上找到一些关于它的文档):

    黄金法则(或不可变法则,如果你愿意的话)是用户不能真正删除具有打开句柄的文件(或dir

    尝试删除或重命名(这似乎与当前问题无关,但事实并非如此)带有打开句柄的 dir 可能会产生不同的结果(取决于句柄的创建方式和API 用于重命名/删除 dir):

    1. 错误 (ERROR_ACCESS_DENIED) - 在未指定 FILE_SHARE_DELETE 时发生(以及其他一些情况)
    2. 没有错误,但该目录仍然存在 - 通常意味着它已被安排删除,并且在其上次打开句柄关闭后会自动消失
    3. 成功并且 dir 被删除。实际上,这不是真的,dir 只是被移动(重命名)为“RECYCLE.BIN”(试图从那里删除它会导致 #1.;尝试真正在第 1st 位置删除它(Shift + Del from 资源管理器))

    我测试了上述场景,试图以各种方式删除/重命名 dir

    • cmd: rmdir /q /s, move /y
    • 资源管理器DelShift + Del
    • Windows 指挥官F8Shift + F8

    我开始研究解决问题的方法,结果遇到了[MS.Docs]: GetFinalPathNameByHandleW function (win32file.GetFinalPathNameByHandle)。玩过:

    >>> import sys
    >>> import os
    >>> import win32api
    >>> import win32file
    >>> import win32con
    >>>
    >>> print("Python {:s} on {:s}\n".format(sys.version, sys.platform))
    Python 3.5.4 (v3.5.4:3f56838, Aug  8 2017, 02:17:05) [MSC v.1900 64 bit (AMD64)] on win32
    
    >>> os.listdir()
    ['code00.py', 'test']
    >>> test_dir = ".\\test"
    >>> os.path.abspath(test_dir)
    'e:\\Work\\Dev\\*\\q049652110\\test'
    >>> h = win32file.CreateFile(test_dir, win32con.GENERIC_READ, win32con.FILE_SHARE_READ | win32con.FILE_SHARE_WRITE | win32con.FILE_SHARE_DELETE, None, win32con.OPEN_EXISTING, win32con.FILE_FLAG_BACKUP_SEMANTICS, None)
    >>> h
    <PyHANDLE:620>
    >>> win32file.GetFinalPathNameByHandle(h, win32con.FILE_NAME_NORMALIZED)
    '\\\\?\\E:\\Work\\Dev\\*\\q049652110\\test'
    >>> test_dir1 = test_dir + "1"
    >>> os.rename(test_dir, test_dir1)
    >>> os.listdir()
    ['code00.py', 'test1']
    >>> win32file.GetFinalPathNameByHandle(h, win32con.FILE_NAME_NORMALIZED)
    '\\\\?\\E:\\Work\\Dev\\*\\q049652110\\test1'
    >>> os.rename(test_dir1, test_dir)
    >>> os.listdir()
    ['code00.py', 'test']
    >>> win32file.GetFinalPathNameByHandle(h, win32con.FILE_NAME_NORMALIZED)
    '\\\\?\\E:\\Work\\Dev\\*\\q049652110\\test'
    >>> os.unlink(test_dir)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    PermissionError: [WinError 5] Access is denied: '.\\test'
    >>> # Delete the dir from elsewhere (don't use os.rmdir since that will only schedule the dir for deletion)
    ...
    >>> os.listdir()
    ['code00.py']
    >>> win32file.GetFinalPathNameByHandle(h, win32con.FILE_NAME_NORMALIZED)
    '\\\\?\\E:\\$RECYCLE.BIN\\S-1-5-21-1906798797-2830956273-3148971768-1002\\$RY7SH8D'
    >>> os.mkdir(test_dir)
    >>> os.listdir()
    ['code00.py', 'test']
    >>> win32file.GetFinalPathNameByHandle(h, win32con.FILE_NAME_NORMALIZED)
    '\\\\?\\E:\\$RECYCLE.BIN\\S-1-5-21-1906798797-2830956273-3148971768-1002\\$RY7SH8D'
    >>> os.rmdir(test_dir) # Since the new "test" dir wasn't open, operation successful
    >>> os.listdir()
    ['code00.py']
    >>> win32file.GetFinalPathNameByHandle(h, win32con.FILE_NAME_NORMALIZED)
    '\\\\?\\E:\\$RECYCLE.BIN\\S-1-5-21-1906798797-2830956273-3148971768-1002\\$RY7SH8D'
    >>> # Restore the dir from RECYCLE.BIN
    ...
    >>> os.listdir()
    ['code00.py', 'test']
    >>> win32file.GetFinalPathNameByHandle(h, win32con.FILE_NAME_NORMALIZED)
    '\\\\?\\E:\\Work\\Dev\\*\\q049652110\\test'
    >>> os.rmdir(test_dir) # Still an open handle, scheduled to be deleted
    >>> os.listdir()
    ['code00.py', 'test']
    >>> win32file.GetFinalPathNameByHandle(h, win32con.FILE_NAME_NORMALIZED)
    '\\\\?\\E:\\Work\\Dev\\*\\q049652110\\test'
    >>> win32api.CloseHandle(h)
    >>> os.listdir()
    ['code00.py'] # After closing the handle the dir was deleted
    >>> h
    <PyHANDLE:0>
    >>> win32file.GetFinalPathNameByHandle(h, win32con.FILE_NAME_NORMALIZED)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    pywintypes.error: (6, 'GetFinalPathNameByHandle', 'The handle is invalid.')
    

    注意:我也尝试了[MS.Docs]: GetFileInformationByHandle function (win32file.GetFileInformationByHandle),但我无法重现该行为,即使使用 3 个 pywintypes.datetime 字段(应该是上次访问/修改时间);重命名/删除 dir 时,没有任何信息发生变化。我没有花时间去调查,我想到了2个可能的原因:

    • 该数据以某种方式存储在 HANDLE“内部”,并且该函数在调用时实际上并不查询 FS(与 GetFinalPathNameByHandle 不同 em>)

    • dir 被重命名/删除时,其父 dir(s)

      的日期字段会更改

    所以,我们似乎有一个赢家。我只贴算法(代码应该比较简单):

    • CreateFile之后,在句柄上调用GetFinalPathNameByHandle并将其保存在一个变量中
    • 在循环中(不断调用 ReadDirectoryChangesW),在每次迭代 (WAIT_TIMEOUT) 再次在句柄上调用 GetFinalPathNameByHandle 并比较结果保存的值;如果它们不同(dir 被重命名/“删除”),则采取适当的措施,例如:
      • 跳出循环
      • 重新打开(重新创建)目录并重新开始
      • 让用户知道

    其他可能的方法(尽管不可取):

    • 不要指定 FILE_SHARE_DELETE(如您所述),这样 dir 将是 不可重命名 / 不可删除
    • 监控父 dir(作为 ReadDirectoryChangesW):

      检索描述指定目录中更改的信息。该函数报告对指定目录本身的更改。

    • 由于父 dir 可能包含您不关心的其他 dirs / 文件,我想到了一个(蹩脚的)解决方法(gainarie) 可能有效也可能无效:
      • 选择一个目录(与我们谈论的完全不同)
      • 为我们的 dir 创建一个 symlink 以监控内部(您控制该 dir,没有其他人会乱用它)李>
      • 监控它(这里我不知道事件是否通过符号链接转发,也不知道在断开链接时监控如何反应)

    关于“事件丢失”,正如我在另一个答案中指定的那样,无法确保所有这些都会被处理,只有最小化丢失的数量的方法。

    【讨论】:

    • 哇,太精确了!我也错过了回收站部分,这确实有道理。
    【解决方案2】:

    好的记录:CristiFati 的解决方案(和问题的解释)成功了。

    实际上有两个问题:

    当简单地删除文件时,它被移到了回收站,所以句柄实际上仍然有效,但在不再跟踪的地方发生了变化。使用GetFinalPathNameByHandle 成功了!我可以检测到删除,并进行相应的处理。

    第二次在不去回收站的情况下删除文件时,出现错误,因为该文件在文件夹中仍然可见但不可访问,因此随后对 CreateFile 的调用失败并出现拒绝访问错误。这是因为之前的文件句柄仍然打开,因此删除还没有完成,只是在排队。所以现在我尝试在调用 CreateFile 之前关闭任何打开的句柄。

    现在是工作代码:

    #
    # Monitors a directory for changes and pass the changes to the queue
    #
    def MonitorDirectory(self, out_queue):
    
        print("Monitoring instance \'{0}\' is watching directory: {1}".format(self.name, self.path))
    
        # File monitor
        FILE_LIST_DIRECTORY = 0x0001
    
        overlapped          = pywintypes.OVERLAPPED()
        overlapped.hEvent   = win32event.CreateEvent(None, False, 0, None)
    
        buffer  = win32file.AllocateReadBuffer(1024 * 64)
    
        # Main loop to keep watching active
        while not self._shutdown.is_set():
    
            # Open directory
            try:    
    
                # Ensure handle is closed so delete event can fire and actually delete the folder
                try:
                    win32file.CloseHandle(hDir)
    
                finally:
                    hDir = win32file.CreateFile(self.path,
                                                FILE_LIST_DIRECTORY,
                                                win32con.FILE_SHARE_READ | win32con.FILE_SHARE_WRITE | win32con.FILE_SHARE_DELETE,
                                                None,
                                                win32con.OPEN_EXISTING,
                                                win32con.FILE_FLAG_BACKUP_SEMANTICS | win32con.FILE_FLAG_OVERLAPPED,
                                                None)
    
                    hDirPath = win32file.GetFinalPathNameByHandle(hDir, win32con.FILE_NAME_NORMALIZED)
    
            except:
    
                print("Directory to monitor does not exist! Waiting...")
    
                # Wait before retry
                time.sleep(1)
    
            else:
    
                # Signal initialized event on queue, so the directory can be initialized
                out_queue.put((0, time.time(), None))
    
                # Monitor directory for changes
                while not self._shutdown.is_set():
    
                    win32file.ReadDirectoryChangesW(hDir,
                                                    buffer,
                                                    True,
                                                    win32con.FILE_NOTIFY_CHANGE_FILE_NAME |
                                                    win32con.FILE_NOTIFY_CHANGE_DIR_NAME |
                                                    win32con.FILE_NOTIFY_CHANGE_ATTRIBUTES |
                                                    win32con.FILE_NOTIFY_CHANGE_SIZE |
                                                    win32con.FILE_NOTIFY_CHANGE_LAST_WRITE |
                                                    win32con.FILE_NOTIFY_CHANGE_SECURITY,
                                                    overlapped,
                                                    None)
    
                    # Wait for the changes
                    rc = win32event.WaitForSingleObject(overlapped.hEvent, 10000)
    
                    # Detect move to Recycle bin
                    try:
                        if hDirPath != win32file.GetFinalPathNameByHandle(hDir, win32con.FILE_NAME_NORMALIZED):
    
                            # Set watched dir as deleted (FileAction: 2 = delete)
                            out_queue.put( (2, time.time(), self.path) )
    
                            # Exits loop and wait for reinit
                            break;
    
                    except:
    
                        print("Error: directory removed or invalid handle...")
                        break;
    
    
                    if rc == win32event.WAIT_OBJECT_0:
    
                        try:
                            bytes_returned = win32file.GetOverlappedResult(hDir, overlapped, True)
    
                        except:
                            print("Error: directory removed or invalid handle...")
                            break;
    
                        else:
    
                            # Get the changes
                            for action, file in win32file.FILE_NOTIFY_INFORMATION(buffer, bytes_returned):
                                out_queue.put((action, time.time(), os.path.join(self.path, file)))
    
                    elif rc == win32event.WAIT_TIMEOUT:
                        print("Monitoring instance \'{0}\': Timeout, no actions".format(self.name))
    
                    else:
                        print("Error?! RC = {0}".format(rc))
                        break
    
        # Done main loop
        print("Monitoring instance \'{0}\' has finished watching directory: {1}".format(self.name, self.path))
    

    【讨论】: