|
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): |
| 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