Skip to content

Commit 74d381e

Browse files
jensbjorgensenJens Jorgensenfantix
authored
Expose libuv uv_fs_event functionality (#474)
* Also updated pyOpenSSL to 22.x Co-authored-by: Jens Jorgensen <[email protected]> Co-authored-by: Fantix King <[email protected]>
1 parent 637a77a commit 74d381e

File tree

7 files changed

+259
-1
lines changed

7 files changed

+259
-1
lines changed

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
'flake8~=3.9.2',
3535
'psutil',
3636
'pycodestyle~=2.7.0',
37-
'pyOpenSSL~=19.0.0',
37+
'pyOpenSSL~=22.0.0',
3838
'mypy>=0.800',
3939
CYTHON_DEPENDENCY,
4040
]

tests/test_fs_event.py

+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import asyncio
2+
import os.path
3+
import tempfile
4+
5+
from uvloop import _testbase as tb
6+
from uvloop.loop import FileSystemEvent
7+
8+
9+
class Test_UV_FS_EVENT_CHANGE(tb.UVTestCase):
10+
async def _file_writer(self):
11+
f = await self.q.get()
12+
while True:
13+
f.write('hello uvloop\n')
14+
f.flush()
15+
x = await self.q.get()
16+
if x is None:
17+
return
18+
19+
def fs_event_setup(self):
20+
self.change_event_count = 0
21+
self.fname = ''
22+
self.q = asyncio.Queue()
23+
24+
def event_cb(self, ev_fname: bytes, evt: FileSystemEvent):
25+
_d, fn = os.path.split(self.fname)
26+
self.assertEqual(ev_fname, fn)
27+
self.assertEqual(evt, FileSystemEvent.CHANGE)
28+
self.change_event_count += 1
29+
if self.change_event_count < 4:
30+
self.q.put_nowait(0)
31+
else:
32+
self.q.put_nowait(None)
33+
34+
def test_fs_event_change(self):
35+
self.fs_event_setup()
36+
37+
async def run(write_task):
38+
self.q.put_nowait(tf)
39+
try:
40+
await asyncio.wait_for(write_task, 4)
41+
except asyncio.TimeoutError:
42+
write_task.cancel()
43+
44+
with tempfile.NamedTemporaryFile('wt') as tf:
45+
self.fname = tf.name.encode()
46+
h = self.loop._monitor_fs(tf.name, self.event_cb)
47+
self.assertFalse(h.cancelled())
48+
49+
self.loop.run_until_complete(run(
50+
self.loop.create_task(self._file_writer())))
51+
h.cancel()
52+
self.assertTrue(h.cancelled())
53+
54+
self.assertEqual(self.change_event_count, 4)
55+
56+
57+
class Test_UV_FS_EVENT_RENAME(tb.UVTestCase):
58+
async def _file_renamer(self):
59+
await self.q.get()
60+
os.rename(os.path.join(self.dname, self.changed_name),
61+
os.path.join(self.dname, self.changed_name + "-new"))
62+
await self.q.get()
63+
64+
def fs_event_setup(self):
65+
self.dname = ''
66+
self.changed_name = "hello_fs_event.txt"
67+
self.changed_set = {self.changed_name, self.changed_name + '-new'}
68+
self.q = asyncio.Queue()
69+
70+
def event_cb(self, ev_fname: bytes, evt: FileSystemEvent):
71+
ev_fname = ev_fname.decode()
72+
self.assertEqual(evt, FileSystemEvent.RENAME)
73+
self.changed_set.remove(ev_fname)
74+
if len(self.changed_set) == 0:
75+
self.q.put_nowait(None)
76+
77+
def test_fs_event_rename(self):
78+
self.fs_event_setup()
79+
80+
async def run(write_task):
81+
self.q.put_nowait(0)
82+
try:
83+
await asyncio.wait_for(write_task, 4)
84+
except asyncio.TimeoutError:
85+
write_task.cancel()
86+
87+
with tempfile.TemporaryDirectory() as td_name:
88+
self.dname = td_name
89+
f = open(os.path.join(td_name, self.changed_name), 'wt')
90+
f.write('hello!')
91+
f.close()
92+
h = self.loop._monitor_fs(td_name, self.event_cb)
93+
self.assertFalse(h.cancelled())
94+
95+
self.loop.run_until_complete(run(
96+
self.loop.create_task(self._file_renamer())))
97+
h.cancel()
98+
self.assertTrue(h.cancelled())
99+
100+
self.assertEqual(len(self.changed_set), 0)

uvloop/handles/fsevent.pxd

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
cdef class UVFSEvent(UVHandle):
2+
cdef:
3+
object callback
4+
bint running
5+
6+
cdef _init(self, Loop loop, object callback, object context)
7+
cdef _close(self)
8+
cdef start(self, char* path, int flags)
9+
cdef stop(self)
10+
11+
@staticmethod
12+
cdef UVFSEvent new(Loop loop, object callback, object context)

uvloop/handles/fsevent.pyx

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import enum
2+
3+
4+
class FileSystemEvent(enum.IntEnum):
5+
RENAME = uv.UV_RENAME
6+
CHANGE = uv.UV_CHANGE
7+
RENAME_CHANGE = RENAME | CHANGE
8+
9+
10+
@cython.no_gc_clear
11+
cdef class UVFSEvent(UVHandle):
12+
cdef _init(self, Loop loop, object callback, object context):
13+
cdef int err
14+
15+
self._start_init(loop)
16+
17+
self._handle = <uv.uv_handle_t*>PyMem_RawMalloc(
18+
sizeof(uv.uv_fs_event_t)
19+
)
20+
if self._handle is NULL:
21+
self._abort_init()
22+
raise MemoryError()
23+
24+
err = uv.uv_fs_event_init(
25+
self._loop.uvloop, <uv.uv_fs_event_t*>self._handle
26+
)
27+
if err < 0:
28+
self._abort_init()
29+
raise convert_error(err)
30+
31+
self._finish_init()
32+
33+
self.running = 0
34+
self.callback = callback
35+
if context is None:
36+
context = Context_CopyCurrent()
37+
self.context = context
38+
39+
cdef start(self, char* path, int flags):
40+
cdef int err
41+
42+
self._ensure_alive()
43+
44+
if self.running == 0:
45+
err = uv.uv_fs_event_start(
46+
<uv.uv_fs_event_t*>self._handle,
47+
__uvfsevent_callback,
48+
path,
49+
flags,
50+
)
51+
if err < 0:
52+
exc = convert_error(err)
53+
self._fatal_error(exc, True)
54+
return
55+
self.running = 1
56+
57+
cdef stop(self):
58+
cdef int err
59+
60+
if not self._is_alive():
61+
self.running = 0
62+
return
63+
64+
if self.running == 1:
65+
err = uv.uv_fs_event_stop(<uv.uv_fs_event_t*>self._handle)
66+
self.running = 0
67+
if err < 0:
68+
exc = convert_error(err)
69+
self._fatal_error(exc, True)
70+
return
71+
72+
cdef _close(self):
73+
try:
74+
self.stop()
75+
finally:
76+
UVHandle._close(<UVHandle>self)
77+
78+
def cancel(self):
79+
self._close()
80+
81+
def cancelled(self):
82+
return self.running == 0
83+
84+
@staticmethod
85+
cdef UVFSEvent new(Loop loop, object callback, object context):
86+
cdef UVFSEvent handle
87+
handle = UVFSEvent.__new__(UVFSEvent)
88+
handle._init(loop, callback, context)
89+
return handle
90+
91+
92+
cdef void __uvfsevent_callback(uv.uv_fs_event_t* handle, const char *filename,
93+
int events, int status) with gil:
94+
if __ensure_handle_data(
95+
<uv.uv_handle_t*>handle, "UVFSEvent callback"
96+
) == 0:
97+
return
98+
99+
cdef:
100+
UVFSEvent fs_event = <UVFSEvent> handle.data
101+
Handle h
102+
103+
try:
104+
h = new_Handle(
105+
fs_event._loop,
106+
fs_event.callback,
107+
(filename, FileSystemEvent(events)),
108+
fs_event.context,
109+
)
110+
h._run()
111+
except BaseException as ex:
112+
fs_event._error(ex, False)

uvloop/includes/uv.pxd

+19
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,10 @@ cdef extern from "uv.h" nogil:
183183
int pid
184184
# ...
185185

186+
ctypedef struct uv_fs_event_t:
187+
void* data
188+
# ...
189+
186190
ctypedef enum uv_req_type:
187191
UV_UNKNOWN_REQ = 0,
188192
UV_REQ,
@@ -215,6 +219,10 @@ cdef extern from "uv.h" nogil:
215219
UV_LEAVE_GROUP = 0,
216220
UV_JOIN_GROUP
217221

222+
cpdef enum uv_fs_event:
223+
UV_RENAME = 1,
224+
UV_CHANGE = 2
225+
218226
const char* uv_strerror(int err)
219227
const char* uv_err_name(int err)
220228

@@ -253,6 +261,10 @@ cdef extern from "uv.h" nogil:
253261
const uv_buf_t* buf,
254262
const system.sockaddr* addr,
255263
unsigned flags) with gil
264+
ctypedef void (*uv_fs_event_cb)(uv_fs_event_t* handle,
265+
const char *filename,
266+
int events,
267+
int status) with gil
256268

257269
# Generic request functions
258270
int uv_cancel(uv_req_t* req)
@@ -397,6 +409,13 @@ cdef extern from "uv.h" nogil:
397409
int uv_poll_start(uv_poll_t* handle, int events, uv_poll_cb cb)
398410
int uv_poll_stop(uv_poll_t* poll)
399411

412+
# FS Event
413+
414+
int uv_fs_event_init(uv_loop_t *loop, uv_fs_event_t *handle)
415+
int uv_fs_event_start(uv_fs_event_t *handle, uv_fs_event_cb cb,
416+
const char *path, unsigned int flags)
417+
int uv_fs_event_stop(uv_fs_event_t *handle)
418+
400419
# Misc
401420

402421
ctypedef struct uv_timeval_t:

uvloop/loop.pxd

+1
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ include "handles/streamserver.pxd"
219219
include "handles/tcp.pxd"
220220
include "handles/pipe.pxd"
221221
include "handles/process.pxd"
222+
include "handles/fsevent.pxd"
222223

223224
include "request.pxd"
224225
include "sslproto.pxd"

uvloop/loop.pyx

+14
Original file line numberDiff line numberDiff line change
@@ -3149,6 +3149,19 @@ cdef class Loop:
31493149
await waiter
31503150
return udp, protocol
31513151

3152+
def _monitor_fs(self, path: str, callback) -> asyncio.Handle:
3153+
cdef:
3154+
UVFSEvent fs_handle
3155+
char* c_str_path
3156+
3157+
self._check_closed()
3158+
fs_handle = UVFSEvent.new(self, callback, None)
3159+
p_bytes = path.encode('UTF-8')
3160+
c_str_path = p_bytes
3161+
flags = 0
3162+
fs_handle.start(c_str_path, flags)
3163+
return fs_handle
3164+
31523165
def _check_default_executor(self):
31533166
if self._executor_shutdown_called:
31543167
raise RuntimeError('Executor shutdown has been called')
@@ -3301,6 +3314,7 @@ include "handles/streamserver.pyx"
33013314
include "handles/tcp.pyx"
33023315
include "handles/pipe.pyx"
33033316
include "handles/process.pyx"
3317+
include "handles/fsevent.pyx"
33043318

33053319
include "request.pyx"
33063320
include "dns.pyx"

0 commit comments

Comments
 (0)