Skip to content
Draft
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
5 changes: 5 additions & 0 deletions Include/fuse_common.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ cdef extern from * nogil: # fuse_common.h should not be included
struct fuse_chan:
pass

struct fuse_pollhandle:
pass

void fuse_pollhandle_destroy(fuse_pollhandle *ph)

struct fuse_loop_config:
int clone_fd
unsigned max_idle_threads
Expand Down
4 changes: 4 additions & 0 deletions Include/fuse_lowlevel.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ cdef extern from "<fuse_lowlevel.h>" nogil:
off_t offset, off_t length, fuse_file_info *fi) except *
void (*readdirplus) (fuse_req_t req, fuse_ino_t ino, size_t size, off_t off,
fuse_file_info *fi) except *
void (*poll) (fuse_req_t req, fuse_ino_t ino, fuse_file_info *fi,
fuse_pollhandle *ph) except *


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

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

# Utility functions
void *fuse_req_userdata(fuse_req_t req)
Expand Down
4 changes: 4 additions & 0 deletions src/pyfuse3/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ class FUSEError(Exception):
def __init__(self, errno: int) -> None: ...
def __str__(self) -> str: ...

class PollHandle:
def __getstate__(self) -> None: ...
def notify(self) -> None: ...

def listdir(path: str) -> List[str]: ...
def syncfs(path: str) -> str: ...
def setxattr(path: str, name: str, value: bytes, namespace: NamespaceT = ...) -> None: ...
Expand Down
60 changes: 60 additions & 0 deletions src/pyfuse3/__init__.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,66 @@ cdef class FUSEError(Exception):
return strerror(self.errno_)


@cython.freelist(10)
cdef class PollHandle:
'''
Opaque handle for delivering poll(2) readiness notifications.

Instances of this class are created by pyfuse3 and passed to
`Operations.poll`. The filesystem may keep a reference and later
call `PollHandle.notify` on the handle to wake up any process currently
blocked in :manpage:`poll(2)`, :manpage:`select(2)` or
:manpage:`epoll_wait(2)` for the corresponding file descriptor.

A single notification is sufficient to clear all pending waiters;
further notifications on the same handle are harmless but redundant.

The underlying ``fuse_pollhandle`` is automatically destroyed when
the Python object is garbage collected, so filesystems should simply
drop the reference when the notification is no longer needed.
'''

cdef fuse_pollhandle *_ph

def __cinit__(self):
self._ph = NULL

def __init__(self):
raise TypeError('PollHandle cannot be instantiated directly')

def __dealloc__(self):
if self._ph is not NULL:
fuse_pollhandle_destroy(self._ph)
self._ph = NULL
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Looking at https://cython.readthedocs.io/en/latest/src/userguide/special_methods.html#finalization-methods-dealloc-and-del, nothing can possibly access the object after __dealloc__ has run, so there's no need to reset the pointer.


def __getstate__(self):
raise PicklingError("PollHandle instances can't be pickled")

def notify(self):
'''
Notify IO readiness for this poll handle.

After this returns, any process waiting in :manpage:`poll(2)`,
:manpage:`select(2)` or :manpage:`epoll_wait(2)` on the
corresponding file descriptor will be woken so it can re-poll
the filesystem for the current readiness mask.

A single notification is enough to clear all pending waiters;
calling this method again on the same handle is harmless but
redundant. The handle remains valid until its Python reference is
dropped, at which point the underlying ``fuse_pollhandle`` is
destroyed.
'''

cdef int ret

with nogil:
ret = fuse_lowlevel_notify_poll(self._ph)

if ret != 0:
raise OSError(-ret, 'fuse_lowlevel_notify_poll returned: ' + strerror(-ret))


def listdir(path):
'''Like `os.listdir`, but releases the GIL.

Expand Down
39 changes: 39 additions & 0 deletions src/pyfuse3/_pyfuse3.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
EntryAttributes,
FileInfo,
FUSEError,
PollHandle,
ReaddirToken,
RequestContext,
SetattrFields,
Expand Down Expand Up @@ -451,6 +452,44 @@ async def fsync(self, fh: FileHandleT, datasync: bool) -> None:

raise FUSEError(errno.ENOSYS)

async def poll(
self,
inode: InodeT,
fh: FileHandleT,
poll_handle: Optional["PollHandle"],
ctx: "RequestContext",
) -> int:
'''Check IO readiness on an open file.

This method is called when a process performs :manpage:`poll(2)`,
:manpage:`select(2)` or :manpage:`epoll_wait(2)` on a file descriptor
backed by *fh* (returned by a prior `open` or `create` call). *inode*
identifies the inode that *fh* refers to.

The method must return the bitwise-or of the currently active poll
events (e.g. ``select.POLLIN``, ``select.POLLOUT``, ``select.POLLPRI``).
If no events are currently ready, return ``0``.

If *poll_handle* is ``None``, the kernel is only asking for the
current readiness mask -- no process is queued waiting for a
notification. The filesystem should just return the current event
bitmask without storing anything.

If *poll_handle* is not ``None``, the kernel is requesting to be
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are the semantics when poll_handle is None?

notified the next time readiness changes. The filesystem should
store the handle and later call `PollHandle.notify` exactly once
when a relevant event becomes available. Each `~Operations.poll`
call produces a fresh handle; storing a new handle implicitly
drops any previously held one (which destroys the underlying
libfuse object).

If this method raises ``FUSEError(errno.ENOSYS)`` (the default),
the kernel will fall back to a default poll implementation and
will not call this handler again for the lifetime of the mount.
'''

raise FUSEError(errno.ENOSYS)

async def opendir(self, inode: InodeT, ctx: "RequestContext") -> FileHandleT:
'''Open the directory with inode *inode*.

Expand Down
34 changes: 34 additions & 0 deletions src/pyfuse3/handlers.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -836,6 +836,40 @@ async def fuse_access_async (_Container c):



cdef void fuse_poll (fuse_req_t req, fuse_ino_t ino, fuse_file_info *fi,
fuse_pollhandle *ph):
cdef _Container c = _Container()
cdef PollHandle py_ph
c.req = req
c.ino = ino
if fi is NULL:
c.fh = 0
else:
c.fh = fi.fh
if ph is NULL:
py_ph = None
else:
py_ph = PollHandle.__new__(PollHandle)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just PollHandle()?

py_ph._ph = ph
save_retval(fuse_poll_async(c, py_ph))

async def fuse_poll_async (_Container c, PollHandle py_ph):
cdef int ret
cdef unsigned revents

ctx = get_request_context(c.req)
try:
result = await operations.poll(c.ino, c.fh, py_ph, ctx)
except FUSEError as e:
ret = fuse_reply_err(c.req, e.errno)
else:
revents = <unsigned> (result if result is not None else 0)
ret = fuse_reply_poll(c.req, revents)

if ret != 0:
log.error('fuse_poll(): fuse_reply_* failed with %s', strerror(-ret))


cdef void fuse_create (fuse_req_t req, fuse_ino_t parent, const_char *name,
mode_t mode, fuse_file_info *fi):
cdef _Container c = _Container()
Expand Down
1 change: 1 addition & 0 deletions src/pyfuse3/internal.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ cdef void init_fuse_ops():
fuse_ops.create = fuse_create
fuse_ops.forget_multi = fuse_forget_multi
fuse_ops.write_buf = fuse_write_buf
fuse_ops.poll = fuse_poll

cdef make_fuse_args(args, fuse_args* f_args):
cdef char* arg
Expand Down
Loading