2121import logging
2222import multiprocessing
2323import os
24+ import select
2425import stat
2526import threading
2627import time
3435 FileInfo ,
3536 FUSEError ,
3637 InodeT ,
38+ PollHandle ,
3739 ReaddirToken ,
3840 RequestContext ,
3941)
@@ -118,6 +120,38 @@ def test_notify_store(testfs):
118120 assert not fs_state .read_called
119121
120122
123+ def test_notify_poll (testfs ):
124+ (mnt_dir , fs_state ) = testfs
125+ path = os .path .join (mnt_dir , 'pollable' )
126+
127+ with open (path , 'rb' , buffering = 0 ) as fh :
128+ poller = select .poll ()
129+ poller .register (fh .fileno (), select .POLLPRI )
130+
131+ events = []
132+
133+ def poll_wait ():
134+ events .extend (poller .poll (5000 ))
135+
136+ thread = threading .Thread (target = poll_wait )
137+ thread .start ()
138+
139+ deadline = time .monotonic () + 5
140+ while time .monotonic () < deadline and not fs_state .poll_handle_received :
141+ time .sleep (0.01 )
142+
143+ assert fs_state .poll_called
144+ assert fs_state .poll_handle_received
145+ assert not events
146+
147+ pyfuse3 .setxattr (mnt_dir , 'command' , b'poll_ready' )
148+ thread .join (5 )
149+ assert not thread .is_alive ()
150+ assert events
151+ assert events [0 ][0 ] == fh .fileno ()
152+ assert events [0 ][1 ] & select .POLLPRI
153+
154+
121155def test_entry_timeout (testfs ):
122156 (mnt_dir , fs_state ) = testfs
123157 fs_state .entry_timeout = 1
@@ -175,11 +209,17 @@ def __init__(self, cross_process):
175209 self .hello_name = b"message"
176210 self .hello_inode = cast (InodeT , pyfuse3 .ROOT_INODE + 1 )
177211 self .hello_data = b"hello world\n "
212+ self .poll_name = b"pollable"
213+ self .poll_inode = cast (InodeT , pyfuse3 .ROOT_INODE + 2 )
214+ self .poll_handle : PollHandle | None = None
178215 self .status = cross_process
179216 self .lookup_cnt = 0
180217 self .status .getattr_called = False
181218 self .status .lookup_called = False
182219 self .status .read_called = False
220+ self .status .poll_called = False
221+ self .status .poll_handle_received = False
222+ self .status .poll_ready = False
183223 self .status .entry_timeout = 99999
184224 self .status .attr_timeout = 99999
185225
@@ -191,6 +231,9 @@ async def getattr(self, inode: InodeT, ctx: RequestContext | None = None) -> Ent
191231 elif inode == self .hello_inode :
192232 entry .st_mode = stat .S_IFREG | 0o644
193233 entry .st_size = len (self .hello_data )
234+ elif inode == self .poll_inode :
235+ entry .st_mode = stat .S_IFREG | 0o644
236+ entry .st_size = 0
194237 else :
195238 raise pyfuse3 .FUSEError (errno .ENOENT )
196239
@@ -212,17 +255,25 @@ async def forget(self, inode_list):
212255 if inode == self .hello_inode :
213256 self .lookup_cnt -= 1
214257 assert self .lookup_cnt >= 0
258+ elif inode == self .poll_inode :
259+ pass
215260 else :
216261 assert inode == pyfuse3 .ROOT_INODE
217262
218263 async def lookup (
219264 self , parent_inode : InodeT , name : bytes , ctx : RequestContext
220265 ) -> EntryAttributes :
221- if parent_inode != pyfuse3 .ROOT_INODE or name != self . hello_name :
266+ if parent_inode != pyfuse3 .ROOT_INODE :
222267 raise pyfuse3 .FUSEError (errno .ENOENT )
223- self . lookup_cnt += 1
268+
224269 self .status .lookup_called = True
225- return await self .getattr (self .hello_inode , ctx )
270+ if name == self .hello_name :
271+ self .lookup_cnt += 1
272+ return await self .getattr (self .hello_inode , ctx )
273+ if name == self .poll_name :
274+ return await self .getattr (self .poll_inode , ctx )
275+
276+ raise pyfuse3 .FUSEError (errno .ENOENT )
226277
227278 async def opendir (self , inode , ctx ):
228279 if inode != pyfuse3 .ROOT_INODE :
@@ -232,23 +283,53 @@ async def opendir(self, inode, ctx):
232283
233284 async def readdir (self , fh : FileHandleT , start_id : int , token : ReaddirToken ) -> None :
234285 assert fh == pyfuse3 .ROOT_INODE
235- if start_id == 0 :
236- pyfuse3 .readdir_reply (token , self .hello_name , await self .getattr (self .hello_inode ), 1 )
237- return
286+ entries = (
287+ (self .hello_name , self .hello_inode ),
288+ (self .poll_name , self .poll_inode ),
289+ )
290+
291+ for idx , (name , inode ) in enumerate (entries ):
292+ if idx < start_id :
293+ continue
294+ if not pyfuse3 .readdir_reply (token , name , await self .getattr (inode ), idx + 1 ):
295+ break
238296
239297 async def open (self , inode , flags , ctx ):
240- if inode != self .hello_inode :
298+ if inode not in ( self .hello_inode , self . poll_inode ) :
241299 raise pyfuse3 .FUSEError (errno .ENOENT )
242300 if flags & os .O_RDWR or flags & os .O_WRONLY :
243301 raise pyfuse3 .FUSEError (errno .EACCES )
244302 # For simplicity, we use the inode as file handle
245303 return FileInfo (fh = FileHandleT (inode ))
246304
247305 async def read (self , fh , off , size ):
306+ if fh == self .poll_inode :
307+ return b''
308+
248309 assert fh == self .hello_inode
249310 self .status .read_called = True
250311 return self .hello_data [off : off + size ]
251312
313+ async def poll (
314+ self ,
315+ inode : InodeT ,
316+ fh : FileHandleT ,
317+ poll_handle : PollHandle | None ,
318+ ctx : RequestContext ,
319+ ) -> int :
320+ assert inode == self .poll_inode
321+ assert fh == self .poll_inode
322+
323+ self .status .poll_called = True
324+ if poll_handle is not None :
325+ self .poll_handle = poll_handle
326+ self .status .poll_handle_received = True
327+
328+ if self .status .poll_ready :
329+ return select .POLLPRI
330+
331+ return 0
332+
252333 async def setxattr (self , inode , name , value , ctx ):
253334 if inode != pyfuse3 .ROOT_INODE or name != b'command' :
254335 raise FUSEError (errno .ENOTSUP )
@@ -267,6 +348,14 @@ async def setxattr(self, inode, name, value, ctx):
267348
268349 elif value == b'terminate' :
269350 pyfuse3 .terminate ()
351+
352+ elif value == b'poll_ready' :
353+ self .status .poll_ready = True
354+ if self .poll_handle is None :
355+ raise FUSEError (errno .EINVAL )
356+ pyfuse3 .notify_poll (self .poll_handle )
357+ self .poll_handle = None
358+
270359 else :
271360 raise FUSEError (errno .EINVAL )
272361
0 commit comments