Commit b67c6c9f authored by Jason Madden's avatar Jason Madden

Fix a lurking GC-related crash with stat watchers on PyPy, detected by CPython...

Fix a lurking GC-related crash with stat watchers on PyPy, detected by CPython re-using the memory much faster.
parent 85a5e56b
...@@ -34,6 +34,10 @@ ...@@ -34,6 +34,10 @@
- The CFFI backend now call the callback of a watcher whose ``args`` attribute is - The CFFI backend now call the callback of a watcher whose ``args`` attribute is
set to ``None``, just like the Cython backend does. It also only set to ``None``, just like the Cython backend does. It also only
allows ``args`` to be a tuple or ``None``, again matching the Cython backend. allows ``args`` to be a tuple or ``None``, again matching the Cython backend.
- PyPy/CFFI: Fix a potential crash when using stat watchers.
- PyPy/CFFI: Encode unicode paths for stat watchers using
:meth:`sys.getfilesystemencoding` like the Cython backend.
1.1rc1 (Nov 14, 2015) 1.1rc1 (Nov 14, 2015)
===================== =====================
......
...@@ -1053,6 +1053,7 @@ cdef public class stat(watcher) [object PyGeventStatObject, type PyGeventStat_Ty ...@@ -1053,6 +1053,7 @@ cdef public class stat(watcher) [object PyGeventStatObject, type PyGeventStat_Ty
self._paths = paths self._paths = paths
else: else:
paths = <bytes>path paths = <bytes>path
self._paths = paths
libev.ev_stat_init(&self._watcher, <void *>gevent_callback_stat, <char*>paths, interval) libev.ev_stat_init(&self._watcher, <void *>gevent_callback_stat, <char*>paths, interval)
self.loop = loop self.loop = loop
if ref: if ref:
......
...@@ -1035,23 +1035,46 @@ class child(watcher): ...@@ -1035,23 +1035,46 @@ class child(watcher):
class stat(watcher): class stat(watcher):
_watcher_type = 'ev_stat' _watcher_type = 'ev_stat'
@staticmethod
def _encode_path(path):
if isinstance(path, bytes):
return path
# encode for the filesystem. Not all systems (e.g., Unix)
# will have an encoding specified
encoding = sys.getfilesystemencoding() or 'utf-8'
try:
path = path.encode(encoding, 'surrogateescape')
except LookupError:
# Can't encode it, and the error handler doesn't
# exist. Probably on Python 2 with an astral character.
# Not sure how to handle this.
raise UnicodeEncodeError("Can't encode path to filesystem encoding")
return path
def __init__(self, _loop, path, interval=0.0, ref=True, priority=None): def __init__(self, _loop, path, interval=0.0, ref=True, priority=None):
if not isinstance(path, bytes): # Store the encoded path in the same attribute that corecext does
# XXX: Filesystem encoding? Python itself has issues here, were they fixed? self._paths = self._encode_path(path)
path = path.encode('utf-8')
# Keep the original path to avoid re-encoding, especially on Python 3
self._path = path
# Although CFFI would automatically convert a bytes object into a char* when
# calling ev_stat_init(..., char*, ...), on PyPy the char* pointer is not
# guaranteed to live past the function call. On CPython, only with a constant/interned
# bytes object is the pointer guaranteed to last path the function call. (And since
# Python 3 is pretty much guaranteed to produce a newly-encoded bytes object above, thats
# rarely the case). Therefore, we must keep a reference to the produced cdata object
# so that the struct ev_stat_watcher's `path` pointer doesn't become invalid/deallocated
self._cpath = ffi.new('char[]', self._paths)
watcher.__init__(self, _loop, ref=ref, priority=priority, watcher.__init__(self, _loop, ref=ref, priority=priority,
# cffi doesn't automatically marshal byte strings to args=(self._cpath,
# char* in the function call; instead it passes an
# empty string or garbage pointer. If the watcher's
# path is incorrect, watching silently fails
# (the underlying call to lstat() keeps erroring out)
args=(ffi.new('char[]', path),
interval)) interval))
@property @property
def path(self): def path(self):
return ffi.string(self._watcher.path) return self._path
@property @property
def attr(self): def attr(self):
......
...@@ -5,6 +5,7 @@ import os ...@@ -5,6 +5,7 @@ import os
import sys import sys
import time import time
#pylint: disable=protected-access
filename = 'tmp.test__core_stat.%s' % os.getpid() filename = 'tmp.test__core_stat.%s' % os.getpid()
...@@ -21,9 +22,8 @@ try: ...@@ -21,9 +22,8 @@ try:
assert os.path.exists(filename), filename assert os.path.exists(filename), filename
def write(): def write():
f = open(filename, 'wb', buffering=0) with open(filename, 'wb', buffering=0) as f:
f.write(b'x') f.write(b'x')
f.close()
start = time.time() start = time.time()
greenlet = gevent.spawn_later(DELAY, write) greenlet = gevent.spawn_later(DELAY, write)
...@@ -32,8 +32,9 @@ try: ...@@ -32,8 +32,9 @@ try:
# which is about 5 seconds. If we go below it's minimum check # which is about 5 seconds. If we go below it's minimum check
# threshold, it bumps it up to the minimum. # threshold, it bumps it up to the minimum.
watcher = hub.loop.stat(filename, interval=-1) watcher = hub.loop.stat(filename, interval=-1)
if hasattr(watcher, 'path'): assert watcher.path == filename, (watcher.path, filename)
assert watcher.path == filename filenames = filename if isinstance(filename, bytes) else filename.encode('ascii')
assert watcher._paths == filenames, (watcher._paths, filenames)
assert watcher.interval == -1 assert watcher.interval == -1
def check_attr(name, none): def check_attr(name, none):
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment