|
5 | 5 | import ctypes.util
|
6 | 6 | import errno
|
7 | 7 | import os
|
| 8 | +import select |
8 | 9 | import struct
|
9 | 10 | import threading
|
10 | 11 | from ctypes import c_char_p, c_int, c_uint32
|
@@ -148,6 +149,9 @@ def __init__(self, path: bytes, *, recursive: bool = False, event_mask: int | No
|
148 | 149 | Inotify._raise_error()
|
149 | 150 | self._inotify_fd = inotify_fd
|
150 | 151 | self._lock = threading.Lock()
|
| 152 | + self._closed = False |
| 153 | + self._waiting_to_read = True |
| 154 | + self._kill_r, self._kill_w = os.pipe() |
151 | 155 |
|
152 | 156 | # Stores the watch descriptor for a given path.
|
153 | 157 | self._wd_for_path: dict[bytes, int] = {}
|
@@ -230,13 +234,19 @@ def remove_watch(self, path: bytes) -> None:
|
230 | 234 | def close(self) -> None:
|
231 | 235 | """Closes the inotify instance and removes all associated watches."""
|
232 | 236 | with self._lock:
|
233 |
| - if self._path in self._wd_for_path: |
234 |
| - wd = self._wd_for_path[self._path] |
235 |
| - inotify_rm_watch(self._inotify_fd, wd) |
| 237 | + if not self._closed: |
| 238 | + self._closed = True |
236 | 239 |
|
237 |
| - # descriptor may be invalid because file was deleted |
238 |
| - with contextlib.suppress(OSError): |
239 |
| - os.close(self._inotify_fd) |
| 240 | + if self._path in self._wd_for_path: |
| 241 | + wd = self._wd_for_path[self._path] |
| 242 | + inotify_rm_watch(self._inotify_fd, wd) |
| 243 | + |
| 244 | + if self._waiting_to_read: |
| 245 | + # inotify_rm_watch() should write data to _inotify_fd and wake |
| 246 | + # the thread, but writing to the kill channel will gaurentee this |
| 247 | + os.write(self._kill_w, b"!") |
| 248 | + else: |
| 249 | + self._close_resources() |
240 | 250 |
|
241 | 251 | def read_events(self, *, event_buffer_size: int = DEFAULT_EVENT_BUFFER_SIZE) -> list[InotifyEvent]:
|
242 | 252 | """Reads events from inotify and yields them."""
|
@@ -276,6 +286,21 @@ def _recursive_simulate(src_path: bytes) -> list[InotifyEvent]:
|
276 | 286 | event_buffer = None
|
277 | 287 | while True:
|
278 | 288 | try:
|
| 289 | + with self._lock: |
| 290 | + if self._closed: |
| 291 | + return [] |
| 292 | + |
| 293 | + self._waiting_to_read = True |
| 294 | + |
| 295 | + select.select([self._inotify_fd, self._kill_r], [], []) |
| 296 | + |
| 297 | + with self._lock: |
| 298 | + self._waiting_to_read = False |
| 299 | + |
| 300 | + if self._closed: |
| 301 | + self._close_resources() |
| 302 | + return [] |
| 303 | + |
279 | 304 | event_buffer = os.read(self._inotify_fd, event_buffer_size)
|
280 | 305 | except OSError as e:
|
281 | 306 | if e.errno == errno.EINTR:
|
@@ -340,6 +365,11 @@ def _recursive_simulate(src_path: bytes) -> list[InotifyEvent]:
|
340 | 365 |
|
341 | 366 | return event_list
|
342 | 367 |
|
| 368 | + def _close_resources(self) -> None: |
| 369 | + os.close(self._inotify_fd) |
| 370 | + os.close(self._kill_r) |
| 371 | + os.close(self._kill_w) |
| 372 | + |
343 | 373 | # Non-synchronized methods.
|
344 | 374 | def _add_dir_watch(self, path: bytes, mask: int, *, recursive: bool) -> None:
|
345 | 375 | """Adds a watch (optionally recursively) for the given directory path
|
|
0 commit comments