Skip to content

Commit

Permalink
Fix issue with observed directory deleted (fixes #570)
Browse files Browse the repository at this point in the history
When observed directory deleted, WindowsApiEmitter stops and release handle to directory.

* Update remove self tests

* Update src/watchdog/observers/winapi.py

Co-Authored-By: Mickaël Schoentgen <contact@tiger-222.fr>

* Fix flake8 error
  • Loading branch information
rrzaripov authored and BoboTiG committed Jun 13, 2019
1 parent a8db635 commit c73eaad
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 18 deletions.
6 changes: 3 additions & 3 deletions src/watchdog/observers/read_directory_changes.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import with_statement

import threading
import os.path
import time
Expand Down Expand Up @@ -72,7 +70,7 @@ def on_thread_stop(self):
close_directory_handle(self._handle)

def _read_events(self):
return read_events(self._handle, self.watch.is_recursive)
return read_events(self._handle, self.watch.path, self.watch.is_recursive)

def queue_events(self, timeout):
winapi_events = self._read_events()
Expand Down Expand Up @@ -123,6 +121,8 @@ def queue_events(self, timeout):
self.queue_event(sub_created_event)
elif winapi_event.is_removed:
self.queue_event(FileDeletedEvent(src_path))
elif winapi_event.is_removed_self:
self.stop()


class WindowsApiObserver(BaseObserver):
Expand Down
55 changes: 48 additions & 7 deletions src/watchdog/observers/winapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@
# Portions of this code were taken from pyfilesystem, which uses the above
# new BSD license.

from __future__ import with_statement

import ctypes.wintypes
from functools import reduce

Expand All @@ -46,7 +44,7 @@
# Invalid handle value.
INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value

# File notification contants.
# File notification constants.
FILE_NOTIFY_CHANGE_FILE_NAME = 0x01
FILE_NOTIFY_CHANGE_DIR_NAME = 0x02
FILE_NOTIFY_CHANGE_ATTRIBUTES = 0x04
Expand All @@ -64,17 +62,21 @@
FILE_SHARE_DELETE = 0x04
OPEN_EXISTING = 3

VOLUME_NAME_NT = 0x02

# File action constants.
FILE_ACTION_CREATED = 1
FILE_ACTION_DELETED = 2
FILE_ACTION_MODIFIED = 3
FILE_ACTION_RENAMED_OLD_NAME = 4
FILE_ACTION_RENAMED_NEW_NAME = 5
FILE_ACTION_DELETED_SELF = 0xFFFE
FILE_ACTION_OVERFLOW = 0xFFFF

# Aliases
FILE_ACTION_ADDED = FILE_ACTION_CREATED
FILE_ACTION_REMOVED = FILE_ACTION_DELETED
FILE_ACTION_REMOVED_SELF = FILE_ACTION_DELETED_SELF

THREAD_TERMINATE = 0x0001

Expand Down Expand Up @@ -219,6 +221,17 @@ def _errcheck_dword(value, func, args):
)


GetFinalPathNameByHandleW = kernel32.GetFinalPathNameByHandleW
GetFinalPathNameByHandleW.restype = ctypes.wintypes.DWORD
GetFinalPathNameByHandleW.errcheck = _errcheck_dword
GetFinalPathNameByHandleW.argtypes = (
ctypes.wintypes.HANDLE, # hFile
ctypes.wintypes.LPWSTR, # lpszFilePath
ctypes.wintypes.DWORD, # cchFilePath
ctypes.wintypes.DWORD, # DWORD
)


class FILE_NOTIFY_INFORMATION(ctypes.Structure):
_fields_ = [("NextEntryOffset", ctypes.wintypes.DWORD),
("Action", ctypes.wintypes.DWORD),
Expand Down Expand Up @@ -270,6 +283,25 @@ def _parse_event_buffer(readBuffer, nBytes):
return results


def _is_observed_path_deleted(handle, path):
# Comparison of observed path and actual path, returned by
# GetFinalPathNameByHandleW. If directory moved to the trash bin, or
# deleted, actual path will not be equal to observed path.
buff = ctypes.create_unicode_buffer(BUFFER_SIZE)
GetFinalPathNameByHandleW(handle, buff, BUFFER_SIZE, VOLUME_NAME_NT)
return buff.value != path


def _generate_observed_path_deleted_event():
# Create synthetic event for notify that observed directory is deleted
path = ctypes.create_unicode_buffer('.')
event = FILE_NOTIFY_INFORMATION(0, FILE_ACTION_DELETED_SELF, len(path), path.value)
event_size = ctypes.sizeof(event)
buff = ctypes.create_string_buffer(BUFFER_SIZE)
ctypes.memmove(buff, ctypes.addressof(event), event_size)
return buff, event_size


def get_directory_handle(path):
"""Returns a Windows handle to the specified directory path."""
return CreateFileW(path, FILE_LIST_DIRECTORY, WATCHDOG_FILE_SHARE_FLAGS,
Expand All @@ -287,7 +319,7 @@ def close_directory_handle(handle):
return


def read_directory_changes(handle, recursive):
def read_directory_changes(handle, path, recursive):
"""Read changes to the directory using the specified directory handle.
http://timgolden.me.uk/pywin32-docs/win32file__ReadDirectoryChangesW_meth.html
Expand All @@ -302,6 +334,11 @@ def read_directory_changes(handle, recursive):
except WindowsError as e:
if e.winerror == ERROR_OPERATION_ABORTED:
return [], 0

# Handle the case when the root path is deleted
if _is_observed_path_deleted(handle, path):
return _generate_observed_path_deleted_event()

raise e

# Python 2/3 compat
Expand Down Expand Up @@ -337,12 +374,16 @@ def is_renamed_old(self):
def is_renamed_new(self):
return self.action == FILE_ACTION_RENAMED_NEW_NAME

@property
def is_removed_self(self):
return self.action == FILE_ACTION_REMOVED_SELF

def __repr__(self):
return ("<%s: action=%d, src_path=%r>" % (
type(self).__name__, self.action, self.src_path))


def read_events(handle, recursive):
buf, nbytes = read_directory_changes(handle, recursive)
def read_events(handle, path, recursive):
buf, nbytes = read_directory_changes(handle, path, recursive)
events = _parse_event_buffer(buf, nbytes)
return [WinAPINativeEvent(action, path) for action, path in events]
return [WinAPINativeEvent(action, src_path) for action, src_path in events]
12 changes: 4 additions & 8 deletions tests/test_emitter.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,19 +238,15 @@ def test_separate_consecutive_moves():
assert isinstance(event, DirModifiedEvent)


@pytest.mark.skipif(platform.is_linux(), reason="bug. inotify will deadlock")
@pytest.mark.skipif(platform.is_windows(), reason="""
WindowsError: [Error 5]
access denied when trying delete directory dir1, because them opened by test
via start_watching.""")
def test_delete_self():
mkdir(p('dir1'))
start_watching(p('dir1'))
rm(p('dir1'), True)

event = event_queue.get(timeout=5)[0]
assert event.src_path == p('dir1')
assert isinstance(event, FileDeletedEvent)
if platform.is_darwin():
event = event_queue.get(timeout=5)[0]
assert event.src_path == p('dir1')
assert isinstance(event, FileDeletedEvent)


@pytest.mark.skipif(platform.is_windows(),
Expand Down

0 comments on commit c73eaad

Please sign in to comment.