Skip to content

Commit 35e41d5

Browse files
committed
add poll notification support
Expose libfuse low-level poll support through pyfuse3 so filesystems can implement poll(2), select(2) and epoll readiness notifications. Add bindings for struct fuse_pollhandle, fuse_reply_poll(), fuse_lowlevel_notify_poll() and fuse_pollhandle_destroy(). Introduce a Python PollHandle wrapper and a notify_poll() helper, allowing a filesystem to retain the poll handle provided by Operations.poll() and notify it later when readiness changes. Wire the low-level FUSE poll callback into Operations.poll(), returning the current readiness mask to the kernel. The default implementation continues to raise ENOSYS so existing filesystems keep the previous fallback behaviour unless they opt in. This is needed by filesystems that emulate pollable kernel interfaces, such as sysfs GPIO value files, where edge events must wake userspace processes waiting for POLLPRI. Fixes: #139 Signed-off-by: Christopher Obbard <christopher.obbard@linaro.org>
1 parent 9e9a1f7 commit 35e41d5

7 files changed

Lines changed: 144 additions & 0 deletions

File tree

Include/fuse_common.pxd

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ cdef extern from * nogil: # fuse_common.h should not be included
4242
struct fuse_chan:
4343
pass
4444

45+
struct fuse_pollhandle:
46+
pass
47+
48+
void fuse_pollhandle_destroy(fuse_pollhandle *ph)
49+
4550
struct fuse_loop_config:
4651
int clone_fd
4752
unsigned max_idle_threads

Include/fuse_lowlevel.pxd

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ cdef extern from "<fuse_lowlevel.h>" nogil:
119119
off_t offset, off_t length, fuse_file_info *fi) except *
120120
void (*readdirplus) (fuse_req_t req, fuse_ino_t ino, size_t size, off_t off,
121121
fuse_file_info *fi) except *
122+
void (*poll) (fuse_req_t req, fuse_ino_t ino, fuse_file_info *fi,
123+
fuse_pollhandle *ph) except *
122124

123125

124126
# Reply functions
@@ -137,6 +139,7 @@ cdef extern from "<fuse_lowlevel.h>" nogil:
137139
fuse_buf_copy_flags flags)
138140
int fuse_reply_statfs(fuse_req_t req, statvfs *stbuf)
139141
int fuse_reply_xattr(fuse_req_t req, size_t count)
142+
int fuse_reply_poll(fuse_req_t req, unsigned revents)
140143

141144
size_t fuse_add_direntry(fuse_req_t req, const_char *buf, size_t bufsize,
142145
const_char *name, struct_stat *stbuf,
@@ -157,6 +160,7 @@ cdef extern from "<fuse_lowlevel.h>" nogil:
157160
fuse_buf_copy_flags flags)
158161
int fuse_lowlevel_notify_retrieve(fuse_session *se, fuse_ino_t ino,
159162
size_t size, off_t offset, void *cookie)
163+
int fuse_lowlevel_notify_poll(fuse_pollhandle *ph)
160164

161165
# Utility functions
162166
void *fuse_req_userdata(fuse_req_t req)

src/pyfuse3/__init__.pyi

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,9 @@ class FUSEError(Exception):
129129
def __init__(self, errno: int) -> None: ...
130130
def __str__(self) -> str: ...
131131

132+
class PollHandle:
133+
def __getstate__(self) -> None: ...
134+
132135
def listdir(path: str) -> List[str]: ...
133136
def syncfs(path: str) -> str: ...
134137
def setxattr(path: str, name: str, value: bytes, namespace: NamespaceT = ...) -> None: ...
@@ -143,6 +146,7 @@ def invalidate_entry_async(
143146
inode_p: InodeT, name: FileNameT, deleted: InodeT = ..., ignore_enoent: bool = ...
144147
) -> None: ...
145148
def notify_store(inode: InodeT, offset: int, data: bytes) -> None: ...
149+
def notify_poll(handle: PollHandle) -> None: ...
146150
def get_sup_groups(pid: int) -> set[int]: ...
147151
def readdir_reply(
148152
token: ReaddirToken, name: FileNameT, attr: EntryAttributes, next_id: int

src/pyfuse3/__init__.pyx

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,40 @@ cdef class FUSEError(Exception):
504504
return strerror(self.errno_)
505505

506506

507+
@cython.freelist(10)
508+
cdef class PollHandle:
509+
'''Opaque handle for delivering poll(2) readiness notifications.
510+
511+
Instances of this class are created by pyfuse3 and passed to
512+
`Operations.poll`. The filesystem may keep a reference and later
513+
pass the handle to `notify_poll` to wake up any process currently
514+
blocked in :manpage:`poll(2)`, :manpage:`select(2)` or
515+
:manpage:`epoll_wait(2)` for the corresponding file descriptor.
516+
517+
A single notification is sufficient to clear all pending waiters;
518+
further notifications on the same handle are harmless but redundant.
519+
520+
The underlying ``fuse_pollhandle`` is automatically destroyed when
521+
the Python object is garbage collected, so filesystems should simply
522+
drop the reference (e.g. by overwriting it with a fresh handle from
523+
a subsequent ``poll`` call) when the notification is no longer
524+
needed.
525+
'''
526+
527+
cdef fuse_pollhandle *_ph
528+
529+
def __cinit__(self):
530+
self._ph = NULL
531+
532+
def __dealloc__(self):
533+
if self._ph is not NULL:
534+
fuse_pollhandle_destroy(self._ph)
535+
self._ph = NULL
536+
537+
def __getstate__(self):
538+
raise PicklingError("PollHandle instances can't be pickled")
539+
540+
507541
def listdir(path):
508542
'''Like `os.listdir`, but releases the GIL.
509543
@@ -952,6 +986,34 @@ def invalidate_entry_async(inode_p, name, deleted=0, ignore_enoent=False):
952986
_notify_queue.put((inode_p, name, deleted, ignore_enoent))
953987

954988

989+
def notify_poll(PollHandle handle not None):
990+
'''Notify IO readiness for *handle*.
991+
992+
*handle* must be a `PollHandle` instance that was previously received
993+
by an `Operations.poll` call. After this returns, any process waiting
994+
in :manpage:`poll(2)`, :manpage:`select(2)` or :manpage:`epoll_wait(2)`
995+
on the corresponding file descriptor will be woken so it can re-poll
996+
the filesystem for the current readiness mask.
997+
998+
A single notification is enough to clear all pending waiters; calling
999+
this function again on the same handle is harmless but redundant.
1000+
The handle remains valid (and may be notified again) until its Python
1001+
reference is dropped, at which point the underlying ``fuse_pollhandle``
1002+
is destroyed.
1003+
'''
1004+
1005+
cdef int ret
1006+
1007+
if handle._ph is NULL:
1008+
raise RuntimeError('PollHandle has been invalidated')
1009+
1010+
with nogil:
1011+
ret = fuse_lowlevel_notify_poll(handle._ph)
1012+
1013+
if ret != 0:
1014+
raise OSError(-ret, 'fuse_lowlevel_notify_poll returned: ' + strerror(-ret))
1015+
1016+
9551017
def notify_store(inode, offset, data):
9561018
'''Store data in kernel page cache
9571019

src/pyfuse3/_pyfuse3.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
EntryAttributes,
3636
FileInfo,
3737
FUSEError,
38+
PollHandle,
3839
ReaddirToken,
3940
RequestContext,
4041
SetattrFields,
@@ -451,6 +452,39 @@ async def fsync(self, fh: FileHandleT, datasync: bool) -> None:
451452

452453
raise FUSEError(errno.ENOSYS)
453454

455+
async def poll(
456+
self,
457+
inode: InodeT,
458+
fh: FileHandleT,
459+
poll_handle: Optional["PollHandle"],
460+
ctx: "RequestContext",
461+
) -> int:
462+
'''Check IO readiness on an open file.
463+
464+
This method is called when a process performs :manpage:`poll(2)`,
465+
:manpage:`select(2)` or :manpage:`epoll_wait(2)` on a file descriptor
466+
backed by *fh* (returned by a prior `open` or `create` call). *inode*
467+
identifies the inode that *fh* refers to.
468+
469+
The method must return the bitwise-or of the currently active poll
470+
events (e.g. ``select.POLLIN``, ``select.POLLOUT``, ``select.POLLPRI``).
471+
If no events are currently ready, return ``0``.
472+
473+
If *poll_handle* is not ``None``, the kernel is requesting to be
474+
notified the next time readiness changes. The filesystem should
475+
store the handle and later call `notify_poll` exactly once when
476+
a relevant event becomes available. Each `~Operations.poll` call
477+
produces a fresh handle; storing a new handle implicitly drops
478+
any previously held one (which destroys the underlying libfuse
479+
object).
480+
481+
If this method raises ``FUSEError(errno.ENOSYS)`` (the default),
482+
the kernel will fall back to a default poll implementation and
483+
will not call this handler again for the lifetime of the mount.
484+
'''
485+
486+
raise FUSEError(errno.ENOSYS)
487+
454488
async def opendir(self, inode: InodeT, ctx: "RequestContext") -> FileHandleT:
455489
'''Open the directory with inode *inode*.
456490

src/pyfuse3/handlers.pxi

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -836,6 +836,40 @@ async def fuse_access_async (_Container c):
836836

837837

838838

839+
cdef void fuse_poll (fuse_req_t req, fuse_ino_t ino, fuse_file_info *fi,
840+
fuse_pollhandle *ph):
841+
cdef _Container c = _Container()
842+
cdef PollHandle py_ph
843+
c.req = req
844+
c.ino = ino
845+
if fi is NULL:
846+
c.fh = 0
847+
else:
848+
c.fh = fi.fh
849+
if ph is NULL:
850+
py_ph = None
851+
else:
852+
py_ph = PollHandle.__new__(PollHandle)
853+
py_ph._ph = ph
854+
save_retval(fuse_poll_async(c, py_ph))
855+
856+
async def fuse_poll_async (_Container c, PollHandle py_ph):
857+
cdef int ret
858+
cdef unsigned revents
859+
860+
ctx = get_request_context(c.req)
861+
try:
862+
result = await operations.poll(c.ino, c.fh, py_ph, ctx)
863+
except FUSEError as e:
864+
ret = fuse_reply_err(c.req, e.errno)
865+
else:
866+
revents = <unsigned> (result if result is not None else 0)
867+
ret = fuse_reply_poll(c.req, revents)
868+
869+
if ret != 0:
870+
log.error('fuse_poll(): fuse_reply_* failed with %s', strerror(-ret))
871+
872+
839873
cdef void fuse_create (fuse_req_t req, fuse_ino_t parent, const_char *name,
840874
mode_t mode, fuse_file_info *fi):
841875
cdef _Container c = _Container()

src/pyfuse3/internal.pxi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ cdef void init_fuse_ops():
6969
fuse_ops.create = fuse_create
7070
fuse_ops.forget_multi = fuse_forget_multi
7171
fuse_ops.write_buf = fuse_write_buf
72+
fuse_ops.poll = fuse_poll
7273

7374
cdef make_fuse_args(args, fuse_args* f_args):
7475
cdef char* arg

0 commit comments

Comments
 (0)