1""":module: watchdog.observers.winapi 2:synopsis: Windows API-Python interface (removes dependency on ``pywin32``). 3:author: [email protected] (Thomas Heller) 4:author: [email protected] (Will McGugan) 5:author: [email protected] (Ryan Kelly) 6:author: [email protected] (Yesudeep Mangalapilly) 7:author: [email protected] (Thomas Amland) 8:author: [email protected] (Mickaël Schoentgen) 9:platforms: windows 10""" 11 12from __future__ import annotations 13 14import contextlib 15import ctypes 16from ctypes.wintypes import BOOL, DWORD, HANDLE, LPCWSTR, LPVOID, LPWSTR 17from dataclasses import dataclass 18from functools import reduce 19from typing import TYPE_CHECKING 20 21if TYPE_CHECKING: 22 from typing import Any 23 24# Invalid handle value. 25INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value 26 27# File notification constants. 28FILE_NOTIFY_CHANGE_FILE_NAME = 0x01 29FILE_NOTIFY_CHANGE_DIR_NAME = 0x02 30FILE_NOTIFY_CHANGE_ATTRIBUTES = 0x04 31FILE_NOTIFY_CHANGE_SIZE = 0x08 32FILE_NOTIFY_CHANGE_LAST_WRITE = 0x010 33FILE_NOTIFY_CHANGE_LAST_ACCESS = 0x020 34FILE_NOTIFY_CHANGE_CREATION = 0x040 35FILE_NOTIFY_CHANGE_SECURITY = 0x0100 36 37FILE_FLAG_BACKUP_SEMANTICS = 0x02000000 38FILE_FLAG_OVERLAPPED = 0x40000000 39FILE_LIST_DIRECTORY = 1 40FILE_SHARE_READ = 0x01 41FILE_SHARE_WRITE = 0x02 42FILE_SHARE_DELETE = 0x04 43OPEN_EXISTING = 3 44 45VOLUME_NAME_NT = 0x02 46 47# File action constants. 48FILE_ACTION_CREATED = 1 49FILE_ACTION_DELETED = 2 50FILE_ACTION_MODIFIED = 3 51FILE_ACTION_RENAMED_OLD_NAME = 4 52FILE_ACTION_RENAMED_NEW_NAME = 5 53FILE_ACTION_DELETED_SELF = 0xFFFE 54FILE_ACTION_OVERFLOW = 0xFFFF 55 56# Aliases 57FILE_ACTION_ADDED = FILE_ACTION_CREATED 58FILE_ACTION_REMOVED = FILE_ACTION_DELETED 59FILE_ACTION_REMOVED_SELF = FILE_ACTION_DELETED_SELF 60 61THREAD_TERMINATE = 0x0001 62 63# IO waiting constants. 64WAIT_ABANDONED = 0x00000080 65WAIT_IO_COMPLETION = 0x000000C0 66WAIT_OBJECT_0 = 0x00000000 67WAIT_TIMEOUT = 0x00000102 68 69# Error codes 70ERROR_OPERATION_ABORTED = 995 71 72 73class OVERLAPPED(ctypes.Structure): 74 _fields_ = ( 75 ("Internal", LPVOID), 76 ("InternalHigh", LPVOID), 77 ("Offset", DWORD), 78 ("OffsetHigh", DWORD), 79 ("Pointer", LPVOID), 80 ("hEvent", HANDLE), 81 ) 82 83 84def _errcheck_bool(value: Any | None, func: Any, args: Any) -> Any: 85 if not value: 86 raise ctypes.WinError() # type: ignore[attr-defined] 87 return args 88 89 90def _errcheck_handle(value: Any | None, func: Any, args: Any) -> Any: 91 if not value: 92 raise ctypes.WinError() # type: ignore[attr-defined] 93 if value == INVALID_HANDLE_VALUE: 94 raise ctypes.WinError() # type: ignore[attr-defined] 95 return args 96 97 98def _errcheck_dword(value: Any | None, func: Any, args: Any) -> Any: 99 if value == 0xFFFFFFFF: 100 raise ctypes.WinError() # type: ignore[attr-defined] 101 return args 102 103 104kernel32 = ctypes.WinDLL("kernel32") # type: ignore[attr-defined] 105 106ReadDirectoryChangesW = kernel32.ReadDirectoryChangesW 107ReadDirectoryChangesW.restype = BOOL 108ReadDirectoryChangesW.errcheck = _errcheck_bool 109ReadDirectoryChangesW.argtypes = ( 110 HANDLE, # hDirectory 111 LPVOID, # lpBuffer 112 DWORD, # nBufferLength 113 BOOL, # bWatchSubtree 114 DWORD, # dwNotifyFilter 115 ctypes.POINTER(DWORD), # lpBytesReturned 116 ctypes.POINTER(OVERLAPPED), # lpOverlapped 117 LPVOID, # FileIOCompletionRoutine # lpCompletionRoutine 118) 119 120CreateFileW = kernel32.CreateFileW 121CreateFileW.restype = HANDLE 122CreateFileW.errcheck = _errcheck_handle 123CreateFileW.argtypes = ( 124 LPCWSTR, # lpFileName 125 DWORD, # dwDesiredAccess 126 DWORD, # dwShareMode 127 LPVOID, # lpSecurityAttributes 128 DWORD, # dwCreationDisposition 129 DWORD, # dwFlagsAndAttributes 130 HANDLE, # hTemplateFile 131) 132 133CloseHandle = kernel32.CloseHandle 134CloseHandle.restype = BOOL 135CloseHandle.argtypes = (HANDLE,) # hObject 136 137CancelIoEx = kernel32.CancelIoEx 138CancelIoEx.restype = BOOL 139CancelIoEx.errcheck = _errcheck_bool 140CancelIoEx.argtypes = ( 141 HANDLE, # hObject 142 ctypes.POINTER(OVERLAPPED), # lpOverlapped 143) 144 145CreateEvent = kernel32.CreateEventW 146CreateEvent.restype = HANDLE 147CreateEvent.errcheck = _errcheck_handle 148CreateEvent.argtypes = ( 149 LPVOID, # lpEventAttributes 150 BOOL, # bManualReset 151 BOOL, # bInitialState 152 LPCWSTR, # lpName 153) 154 155SetEvent = kernel32.SetEvent 156SetEvent.restype = BOOL 157SetEvent.errcheck = _errcheck_bool 158SetEvent.argtypes = (HANDLE,) # hEvent 159 160WaitForSingleObjectEx = kernel32.WaitForSingleObjectEx 161WaitForSingleObjectEx.restype = DWORD 162WaitForSingleObjectEx.errcheck = _errcheck_dword 163WaitForSingleObjectEx.argtypes = ( 164 HANDLE, # hObject 165 DWORD, # dwMilliseconds 166 BOOL, # bAlertable 167) 168 169CreateIoCompletionPort = kernel32.CreateIoCompletionPort 170CreateIoCompletionPort.restype = HANDLE 171CreateIoCompletionPort.errcheck = _errcheck_handle 172CreateIoCompletionPort.argtypes = ( 173 HANDLE, # FileHandle 174 HANDLE, # ExistingCompletionPort 175 LPVOID, # CompletionKey 176 DWORD, # NumberOfConcurrentThreads 177) 178 179GetQueuedCompletionStatus = kernel32.GetQueuedCompletionStatus 180GetQueuedCompletionStatus.restype = BOOL 181GetQueuedCompletionStatus.errcheck = _errcheck_bool 182GetQueuedCompletionStatus.argtypes = ( 183 HANDLE, # CompletionPort 184 LPVOID, # lpNumberOfBytesTransferred 185 LPVOID, # lpCompletionKey 186 ctypes.POINTER(OVERLAPPED), # lpOverlapped 187 DWORD, # dwMilliseconds 188) 189 190PostQueuedCompletionStatus = kernel32.PostQueuedCompletionStatus 191PostQueuedCompletionStatus.restype = BOOL 192PostQueuedCompletionStatus.errcheck = _errcheck_bool 193PostQueuedCompletionStatus.argtypes = ( 194 HANDLE, # CompletionPort 195 DWORD, # lpNumberOfBytesTransferred 196 DWORD, # lpCompletionKey 197 ctypes.POINTER(OVERLAPPED), # lpOverlapped 198) 199 200 201GetFinalPathNameByHandleW = kernel32.GetFinalPathNameByHandleW 202GetFinalPathNameByHandleW.restype = DWORD 203GetFinalPathNameByHandleW.errcheck = _errcheck_dword 204GetFinalPathNameByHandleW.argtypes = ( 205 HANDLE, # hFile 206 LPWSTR, # lpszFilePath 207 DWORD, # cchFilePath 208 DWORD, # DWORD 209) 210 211 212class FileNotifyInformation(ctypes.Structure): 213 _fields_ = ( 214 ("NextEntryOffset", DWORD), 215 ("Action", DWORD), 216 ("FileNameLength", DWORD), 217 ("FileName", (ctypes.c_char * 1)), 218 ) 219 220 221LPFNI = ctypes.POINTER(FileNotifyInformation) 222 223 224# We don't need to recalculate these flags every time a call is made to 225# the win32 API functions. 226WATCHDOG_FILE_FLAGS = FILE_FLAG_BACKUP_SEMANTICS 227WATCHDOG_FILE_SHARE_FLAGS = reduce( 228 lambda x, y: x | y, 229 [ 230 FILE_SHARE_READ, 231 FILE_SHARE_WRITE, 232 FILE_SHARE_DELETE, 233 ], 234) 235WATCHDOG_FILE_NOTIFY_FLAGS = reduce( 236 lambda x, y: x | y, 237 [ 238 FILE_NOTIFY_CHANGE_FILE_NAME, 239 FILE_NOTIFY_CHANGE_DIR_NAME, 240 FILE_NOTIFY_CHANGE_ATTRIBUTES, 241 FILE_NOTIFY_CHANGE_SIZE, 242 FILE_NOTIFY_CHANGE_LAST_WRITE, 243 FILE_NOTIFY_CHANGE_SECURITY, 244 FILE_NOTIFY_CHANGE_LAST_ACCESS, 245 FILE_NOTIFY_CHANGE_CREATION, 246 ], 247) 248 249# ReadDirectoryChangesW buffer length. 250# To handle cases with lot of changes, this seems the highest safest value we can use. 251# Note: it will fail with ERROR_INVALID_PARAMETER when it is greater than 64 KB and 252# the application is monitoring a directory over the network. 253# This is due to a packet size limitation with the underlying file sharing protocols. 254# https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw#remarks 255BUFFER_SIZE = 64000 256 257# Buffer length for path-related stuff. 258# Introduced to keep the old behavior when we bumped BUFFER_SIZE from 2048 to 64000 in v1.0.0. 259PATH_BUFFER_SIZE = 2048 260 261 262def _parse_event_buffer(read_buffer: bytes, n_bytes: int) -> list[tuple[int, str]]: 263 results = [] 264 while n_bytes > 0: 265 fni = ctypes.cast(read_buffer, LPFNI)[0] # type: ignore[arg-type] 266 ptr = ctypes.addressof(fni) + FileNotifyInformation.FileName.offset 267 filename = ctypes.string_at(ptr, fni.FileNameLength) 268 results.append((fni.Action, filename.decode("utf-16"))) 269 num_to_skip = fni.NextEntryOffset 270 if num_to_skip <= 0: 271 break 272 read_buffer = read_buffer[num_to_skip:] 273 n_bytes -= num_to_skip # num_to_skip is long. n_bytes should be long too. 274 return results 275 276 277def _is_observed_path_deleted(handle: HANDLE, path: str) -> bool: 278 # Comparison of observed path and actual path, returned by 279 # GetFinalPathNameByHandleW. If directory moved to the trash bin, or 280 # deleted, actual path will not be equal to observed path. 281 buff = ctypes.create_unicode_buffer(PATH_BUFFER_SIZE) 282 GetFinalPathNameByHandleW(handle, buff, PATH_BUFFER_SIZE, VOLUME_NAME_NT) 283 return buff.value != path 284 285 286def _generate_observed_path_deleted_event() -> tuple[bytes, int]: 287 # Create synthetic event for notify that observed directory is deleted 288 path = ctypes.create_unicode_buffer(".") 289 event = FileNotifyInformation(0, FILE_ACTION_DELETED_SELF, len(path), path.value.encode("utf-8")) 290 event_size = ctypes.sizeof(event) 291 buff = ctypes.create_string_buffer(PATH_BUFFER_SIZE) 292 ctypes.memmove(buff, ctypes.addressof(event), event_size) 293 return buff.raw, event_size 294 295 296def get_directory_handle(path: str) -> HANDLE: 297 """Returns a Windows handle to the specified directory path.""" 298 return CreateFileW( 299 path, 300 FILE_LIST_DIRECTORY, 301 WATCHDOG_FILE_SHARE_FLAGS, 302 None, 303 OPEN_EXISTING, 304 WATCHDOG_FILE_FLAGS, 305 None, 306 ) 307 308 309def close_directory_handle(handle: HANDLE) -> None: 310 try: 311 CancelIoEx(handle, None) # force ReadDirectoryChangesW to return 312 CloseHandle(handle) 313 except OSError: 314 with contextlib.suppress(Exception): 315 CloseHandle(handle) 316 317 318def read_directory_changes(handle: HANDLE, path: str, *, recursive: bool) -> tuple[bytes, int]: 319 """Read changes to the directory using the specified directory handle. 320 321 https://timgolden.me.uk/pywin32-docs/win32file__ReadDirectoryChangesW_meth.html 322 """ 323 event_buffer = ctypes.create_string_buffer(BUFFER_SIZE) 324 nbytes = DWORD() 325 try: 326 ReadDirectoryChangesW( 327 handle, 328 ctypes.byref(event_buffer), 329 len(event_buffer), 330 recursive, 331 WATCHDOG_FILE_NOTIFY_FLAGS, 332 ctypes.byref(nbytes), 333 None, 334 None, 335 ) 336 except OSError as e: 337 if e.winerror == ERROR_OPERATION_ABORTED: # type: ignore[attr-defined] 338 return event_buffer.raw, 0 339 340 # Handle the case when the root path is deleted 341 if _is_observed_path_deleted(handle, path): 342 return _generate_observed_path_deleted_event() 343 344 raise 345 346 return event_buffer.raw, int(nbytes.value) 347 348 349@dataclass(unsafe_hash=True) 350class WinAPINativeEvent: 351 action: int 352 src_path: str 353 354 @property 355 def is_added(self) -> bool: 356 return self.action == FILE_ACTION_CREATED 357 358 @property 359 def is_removed(self) -> bool: 360 return self.action == FILE_ACTION_REMOVED 361 362 @property 363 def is_modified(self) -> bool: 364 return self.action == FILE_ACTION_MODIFIED 365 366 @property 367 def is_renamed_old(self) -> bool: 368 return self.action == FILE_ACTION_RENAMED_OLD_NAME 369 370 @property 371 def is_renamed_new(self) -> bool: 372 return self.action == FILE_ACTION_RENAMED_NEW_NAME 373 374 @property 375 def is_removed_self(self) -> bool: 376 return self.action == FILE_ACTION_REMOVED_SELF 377 378 379def read_events(handle: HANDLE, path: str, *, recursive: bool) -> list[WinAPINativeEvent]: 380 buf, nbytes = read_directory_changes(handle, path, recursive=recursive) 381 events = _parse_event_buffer(buf, nbytes) 382 return [WinAPINativeEvent(action, src_path) for action, src_path in events] 383