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