Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix reading inotify file descriptor after closing it. #1081

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 21 additions & 12 deletions src/watchdog/observers/inotify_c.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
import struct
import threading
from ctypes import c_char_p, c_int, c_uint32
from functools import partial, reduce
from typing import TYPE_CHECKING, Any, Callable
from functools import reduce
from typing import TYPE_CHECKING

from watchdog.utils import UnsupportedLibcError

Expand Down Expand Up @@ -150,16 +150,26 @@ def __init__(self, path: bytes, *, recursive: bool = False, event_mask: int | No
self._inotify_fd = inotify_fd
self._lock = threading.Lock()
self._closed = False
self._waiting_to_read = True
self._is_reading = True
self._kill_r, self._kill_w = os.pipe()

# _check_inotify_fd will return true if we can read _inotify_fd without blocking
if hasattr(select, "poll"):
self._poller = select.poll()
self._poller.register(self._inotify_fd, select.POLLIN)
self._poller.register(self._kill_r, select.POLLIN)
self._poll: Callable[[], Any] = partial(self._poller.poll)

def do_poll() -> bool:
return any(fd == self._inotify_fd for fd, _ in self._poller.poll())

self._check_inotify_fd = do_poll
else:
self._poll = partial(select.select, (self._inotify_fd, self._kill_r))

def do_select() -> bool:
result = select.select([self._inotify_fd, self._kill_r], [], [])
return self._inotify_fd in result[0]

self._check_inotify_fd = do_select

# Stores the watch descriptor for a given path.
self._wd_for_path: dict[bytes, int] = {}
Expand Down Expand Up @@ -249,7 +259,7 @@ def close(self) -> None:
wd = self._wd_for_path[self._path]
inotify_rm_watch(self._inotify_fd, wd)

if self._waiting_to_read:
if self._is_reading:
# inotify_rm_watch() should write data to _inotify_fd and wake
# the thread, but writing to the kill channel will gaurentee this
os.write(self._kill_w, b"!")
Expand Down Expand Up @@ -291,25 +301,24 @@ def _recursive_simulate(src_path: bytes) -> list[InotifyEvent]:
events.append(e)
return events

event_buffer = None
event_buffer = b""
while True:
try:
with self._lock:
if self._closed:
return []

self._waiting_to_read = True
self._is_reading = True

self._poll()
if self._check_inotify_fd():
event_buffer = os.read(self._inotify_fd, event_buffer_size)

with self._lock:
self._waiting_to_read = False
self._is_reading = False

if self._closed:
self._close_resources()
return []

event_buffer = os.read(self._inotify_fd, event_buffer_size)
except OSError as e:
if e.errno == errno.EINTR:
continue
Expand Down
2 changes: 1 addition & 1 deletion tests/test_inotify_c.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ def register(self, fd, *args, **kwargs):

def poll(self, *args, **kwargs):
if self._fake:
return None
return [(inotify_fd, select.POLLIN)]
return self._orig.poll(*args, **kwargs)

os_read_bkp = os.read
Expand Down
Loading