Skip to content

Commit 3d1b888

Browse files
authored
[inotify] Use of select.poll() instead of deprecated select.select() (#1078)
* Use `select.poll()` if available. Details: As stated in the `select()` man page: ``` WARNING: select() can monitor only file descriptors numbers that are less than FD_SETSIZE (1024)—an unreasonably low limit for many modern applications—and this limitation will not change. ``` This can lead to `ValueError: filedescriptor out of range in select()` when using watchdog. Following the advice of the `select()` man page, we use `select.poll()` instead, if available. The call to `select()` used as a fallback. * Add changelog entry for `select.poll()` usage. * Add a unit-test to ensure that we can handle file descriptors >1024.
1 parent 6a4f1cf commit 3d1b888

File tree

3 files changed

+54
-5
lines changed

3 files changed

+54
-5
lines changed

changelog.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Changelog
88

99
2024-xx-xx • `full history <https://github.com/gorakhargosh/watchdog/compare/v5.0.3...HEAD>`__
1010

11-
-
11+
- [inotify] Use of ``select.poll()`` instead of deprecated ``select.select()``, if available. (`#1078 <https://github.com/gorakhargosh/watchdog/pull/1078>`__)
1212
- Thanks to our beloved contributors: @BoboTiG, @
1313

1414
5.0.3

src/watchdog/observers/inotify_c.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
import struct
1010
import threading
1111
from ctypes import c_char_p, c_int, c_uint32
12-
from functools import reduce
13-
from typing import TYPE_CHECKING
12+
from functools import partial, reduce
13+
from typing import TYPE_CHECKING, Any, Callable
1414

1515
from watchdog.utils import UnsupportedLibcError
1616

@@ -153,6 +153,14 @@ def __init__(self, path: bytes, *, recursive: bool = False, event_mask: int | No
153153
self._waiting_to_read = True
154154
self._kill_r, self._kill_w = os.pipe()
155155

156+
if hasattr(select, "poll"):
157+
self._poller = select.poll()
158+
self._poller.register(self._inotify_fd, select.POLLIN)
159+
self._poller.register(self._kill_r, select.POLLIN)
160+
self._poll: Callable[[], Any] = partial(self._poller.poll)
161+
else:
162+
self._poll = partial(select.select, (self._inotify_fd, self._kill_r))
163+
156164
# Stores the watch descriptor for a given path.
157165
self._wd_for_path: dict[bytes, int] = {}
158166
self._path_for_wd: dict[int, bytes] = {}
@@ -292,7 +300,7 @@ def _recursive_simulate(src_path: bytes) -> list[InotifyEvent]:
292300

293301
self._waiting_to_read = True
294302

295-
select.select([self._inotify_fd, self._kill_r], [], [])
303+
self._poll()
296304

297305
with self._lock:
298306
self._waiting_to_read = False

tests/test_inotify_c.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations
22

3+
from contextlib import ExitStack
4+
35
import pytest
46

57
from watchdog.utils import platform
@@ -64,6 +66,24 @@ def fakeselect(read_list, *args, **kwargs):
6466
return [inotify_fd], [], []
6567
return select_bkp(read_list, *args, **kwargs)
6668

69+
poll_bkp = select.poll
70+
71+
class Fakepoll:
72+
def __init__(self):
73+
self._orig = poll_bkp()
74+
self._fake = False
75+
76+
def register(self, fd, *args, **kwargs):
77+
if fd == inotify_fd:
78+
self._fake = True
79+
return None
80+
return self._orig.register(fd, *args, **kwargs)
81+
82+
def poll(self, *args, **kwargs):
83+
if self._fake:
84+
return None
85+
return self._orig.poll(*args, **kwargs)
86+
6787
os_read_bkp = os.read
6888

6989
def fakeread(fd, length):
@@ -101,8 +121,9 @@ def inotify_rm_watch(fd, wd):
101121
mock4 = patch.object(inotify_c, "inotify_add_watch", new=inotify_add_watch)
102122
mock5 = patch.object(inotify_c, "inotify_rm_watch", new=inotify_rm_watch)
103123
mock6 = patch.object(select, "select", new=fakeselect)
124+
mock7 = patch.object(select, "poll", new=Fakepoll)
104125

105-
with mock1, mock2, mock3, mock4, mock5, mock6:
126+
with mock1, mock2, mock3, mock4, mock5, mock6, mock7:
106127
start_watching(path=p(""))
107128
# Watchdog Events
108129
for evt_cls in [DirCreatedEvent, DirDeletedEvent] * 2:
@@ -168,3 +189,23 @@ def test_event_equality(p: P) -> None:
168189
assert event1 == event2
169190
assert event1 != event3
170191
assert event2 != event3
192+
193+
194+
def test_select_fd(p: P, event_queue: TestEventQueue, start_watching: StartWatching) -> None:
195+
# We open a file 2048 times to ensure that we exhaust 1024 file
196+
# descriptors, the limit of a select() call.
197+
path = p("new_file")
198+
with open(path, "a"):
199+
pass
200+
with ExitStack() as stack:
201+
for _i in range(2048):
202+
stack.enter_context(open(path))
203+
204+
# Watch this file for deletion (copied from `test_watch_file`)
205+
path = p("this_is_a_file")
206+
with open(path, "a"):
207+
pass
208+
start_watching(path=path)
209+
os.remove(path)
210+
event, _ = event_queue.get(timeout=5)
211+
assert repr(event)

0 commit comments

Comments
 (0)