Bugfixes:
- GraphicsItem.pixelVectors copies cached results before returning - Multiprocess fixes for Windows: - mmap/shm uses anonymous maps rather than tempfiles - avoid use of getppid and setpgrp - work around hmac authentication bug (use os.urandom to generate key)
This commit is contained in:
parent
6903886b3a
commit
9a9fc15873
@ -4,7 +4,7 @@ PyQtGraph - Scientific Graphics and GUI Library for Python
|
|||||||
www.pyqtgraph.org
|
www.pyqtgraph.org
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = None
|
__version__ = '0.9.5'
|
||||||
|
|
||||||
### import all the goodies and add some helper functions for easy CLI use
|
### import all the goodies and add some helper functions for easy CLI use
|
||||||
|
|
||||||
@ -49,6 +49,8 @@ CONFIG_OPTIONS = {
|
|||||||
'background': (0, 0, 0), ## default background for GraphicsWidget
|
'background': (0, 0, 0), ## default background for GraphicsWidget
|
||||||
'antialias': False,
|
'antialias': False,
|
||||||
'editorCommand': None, ## command used to invoke code editor from ConsoleWidgets
|
'editorCommand': None, ## command used to invoke code editor from ConsoleWidgets
|
||||||
|
'useWeave': True, ## Use weave to speed up some operations, if it is available
|
||||||
|
'weaveDebug': False, ## Print full error message if weave compile fails
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@ SI_PREFIXES_ASCII = 'yzafpnum kMGTPEZY'
|
|||||||
|
|
||||||
|
|
||||||
from .Qt import QtGui, QtCore, USE_PYSIDE
|
from .Qt import QtGui, QtCore, USE_PYSIDE
|
||||||
|
from pyqtgraph import getConfigOption
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import decimal, re
|
import decimal, re
|
||||||
import ctypes
|
import ctypes
|
||||||
@ -30,9 +31,10 @@ import ctypes
|
|||||||
try:
|
try:
|
||||||
import scipy.ndimage
|
import scipy.ndimage
|
||||||
HAVE_SCIPY = True
|
HAVE_SCIPY = True
|
||||||
|
WEAVE_DEBUG = getConfigOption('weaveDebug')
|
||||||
try:
|
try:
|
||||||
import scipy.weave
|
import scipy.weave
|
||||||
USE_WEAVE = True
|
USE_WEAVE = getConfigOption('useWeave')
|
||||||
except:
|
except:
|
||||||
USE_WEAVE = False
|
USE_WEAVE = False
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@ -631,7 +633,8 @@ def rescaleData(data, scale, offset, dtype=None):
|
|||||||
data = newData.reshape(data.shape)
|
data = newData.reshape(data.shape)
|
||||||
except:
|
except:
|
||||||
if USE_WEAVE:
|
if USE_WEAVE:
|
||||||
debug.printExc("Error; disabling weave.")
|
if WEAVE_DEBUG:
|
||||||
|
debug.printExc("Error; disabling weave.")
|
||||||
USE_WEAVE = False
|
USE_WEAVE = False
|
||||||
|
|
||||||
#p = np.poly1d([scale, -offset*scale])
|
#p = np.poly1d([scale, -offset*scale])
|
||||||
@ -1871,4 +1874,4 @@ def pseudoScatter(data, spacing=None, shuffle=True):
|
|||||||
|
|
||||||
yvals[i] = y
|
yvals[i] = y
|
||||||
|
|
||||||
return yvals[np.argsort(inds)] ## un-shuffle values before returning
|
return yvals[np.argsort(inds)] ## un-shuffle values before returning
|
||||||
|
@ -197,14 +197,14 @@ class GraphicsItem(object):
|
|||||||
|
|
||||||
## check local cache
|
## check local cache
|
||||||
if direction is None and dt == self._pixelVectorCache[0]:
|
if direction is None and dt == self._pixelVectorCache[0]:
|
||||||
return self._pixelVectorCache[1]
|
return map(Point, self._pixelVectorCache[1]) ## return a *copy*
|
||||||
|
|
||||||
## check global cache
|
## check global cache
|
||||||
key = (dt.m11(), dt.m21(), dt.m31(), dt.m12(), dt.m22(), dt.m32(), dt.m31(), dt.m32())
|
key = (dt.m11(), dt.m21(), dt.m31(), dt.m12(), dt.m22(), dt.m32(), dt.m31(), dt.m32())
|
||||||
pv = self._pixelVectorGlobalCache.get(key, None)
|
pv = self._pixelVectorGlobalCache.get(key, None)
|
||||||
if pv is not None:
|
if pv is not None:
|
||||||
self._pixelVectorCache = [dt, pv]
|
self._pixelVectorCache = [dt, pv]
|
||||||
return pv
|
return map(Point,pv) ## return a *copy*
|
||||||
|
|
||||||
|
|
||||||
if direction is None:
|
if direction is None:
|
||||||
@ -547,4 +547,4 @@ class GraphicsItem(object):
|
|||||||
|
|
||||||
#def update(self):
|
#def update(self):
|
||||||
#self._qtBaseClass.update(self)
|
#self._qtBaseClass.update(self)
|
||||||
#print "Update:", self
|
#print "Update:", self
|
||||||
|
@ -1783,8 +1783,7 @@ class LineSegmentROI(ROI):
|
|||||||
dh = h2-h1
|
dh = h2-h1
|
||||||
if dh.length() == 0:
|
if dh.length() == 0:
|
||||||
return p
|
return p
|
||||||
pxv = self.pixelVectors(h2-h1)[1]
|
pxv = self.pixelVectors(dh)[1]
|
||||||
|
|
||||||
if pxv is None:
|
if pxv is None:
|
||||||
return p
|
return p
|
||||||
|
|
||||||
|
@ -2,8 +2,10 @@
|
|||||||
import sys, pickle, os
|
import sys, pickle, os
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
os.setpgrp() ## prevents signals (notably keyboard interrupt) being forwarded from parent to this process
|
if hasattr(os, 'setpgrp'):
|
||||||
name, port, authkey, targetStr, path = pickle.load(sys.stdin)
|
os.setpgrp() ## prevents signals (notably keyboard interrupt) being forwarded from parent to this process
|
||||||
|
name, port, authkey, ppid, targetStr, path = pickle.load(sys.stdin)
|
||||||
|
#print "key:", ' '.join([str(ord(x)) for x in authkey])
|
||||||
if path is not None:
|
if path is not None:
|
||||||
## rewrite sys.path without assigning a new object--no idea who already has a reference to the existing list.
|
## rewrite sys.path without assigning a new object--no idea who already has a reference to the existing list.
|
||||||
while len(sys.path) > 0:
|
while len(sys.path) > 0:
|
||||||
@ -12,5 +14,5 @@ if __name__ == '__main__':
|
|||||||
#import pyqtgraph
|
#import pyqtgraph
|
||||||
#import pyqtgraph.multiprocess.processes
|
#import pyqtgraph.multiprocess.processes
|
||||||
target = pickle.loads(targetStr) ## unpickling the target should import everything we need
|
target = pickle.loads(targetStr) ## unpickling the target should import everything we need
|
||||||
target(name, port, authkey)
|
target(name, port, authkey, ppid)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
@ -54,12 +54,13 @@ class Process(RemoteEventHandler):
|
|||||||
executable = sys.executable
|
executable = sys.executable
|
||||||
|
|
||||||
## random authentication key
|
## random authentication key
|
||||||
authkey = ''.join([chr(random.getrandbits(7)) for i in range(20)])
|
authkey = os.urandom(20)
|
||||||
|
#print "key:", ' '.join([str(ord(x)) for x in authkey])
|
||||||
## Listen for connection from remote process (and find free port number)
|
## Listen for connection from remote process (and find free port number)
|
||||||
port = 10000
|
port = 10000
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
|
## hmac authentication appears to be broken on windows (says AuthenticationError: digest received was wrong)
|
||||||
l = multiprocessing.connection.Listener(('localhost', int(port)), authkey=authkey)
|
l = multiprocessing.connection.Listener(('localhost', int(port)), authkey=authkey)
|
||||||
break
|
break
|
||||||
except socket.error as ex:
|
except socket.error as ex:
|
||||||
@ -73,7 +74,8 @@ class Process(RemoteEventHandler):
|
|||||||
self.proc = subprocess.Popen((executable, bootstrap), stdin=subprocess.PIPE)
|
self.proc = subprocess.Popen((executable, bootstrap), stdin=subprocess.PIPE)
|
||||||
targetStr = pickle.dumps(target) ## double-pickle target so that child has a chance to
|
targetStr = pickle.dumps(target) ## double-pickle target so that child has a chance to
|
||||||
## set its sys.path properly before unpickling the target
|
## set its sys.path properly before unpickling the target
|
||||||
pickle.dump((name+'_child', port, authkey, targetStr, sysPath), self.proc.stdin)
|
pid = os.getpid() # we must sent pid to child because windows does not have getppid
|
||||||
|
pickle.dump((name+'_child', port, authkey, pid, targetStr, sysPath), self.proc.stdin)
|
||||||
self.proc.stdin.close()
|
self.proc.stdin.close()
|
||||||
|
|
||||||
## open connection for remote process
|
## open connection for remote process
|
||||||
@ -92,10 +94,11 @@ class Process(RemoteEventHandler):
|
|||||||
time.sleep(0.05)
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
|
||||||
def startEventLoop(name, port, authkey):
|
def startEventLoop(name, port, authkey, ppid):
|
||||||
conn = multiprocessing.connection.Client(('localhost', int(port)), authkey=authkey)
|
conn = multiprocessing.connection.Client(('localhost', int(port)), authkey=authkey)
|
||||||
global HANDLER
|
global HANDLER
|
||||||
HANDLER = RemoteEventHandler(conn, name, os.getppid())
|
#ppid = 0 if not hasattr(os, 'getppid') else os.getppid()
|
||||||
|
HANDLER = RemoteEventHandler(conn, name, ppid)
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
HANDLER.processRequests() # exception raised when the loop should exit
|
HANDLER.processRequests() # exception raised when the loop should exit
|
||||||
@ -161,6 +164,7 @@ class ForkedProcess(RemoteEventHandler):
|
|||||||
proxyId = LocalObjectProxy.registerObject(v)
|
proxyId = LocalObjectProxy.registerObject(v)
|
||||||
proxyIDs[k] = proxyId
|
proxyIDs[k] = proxyId
|
||||||
|
|
||||||
|
ppid = os.getpid() # write this down now; windows doesn't have getppid
|
||||||
pid = os.fork()
|
pid = os.fork()
|
||||||
if pid == 0:
|
if pid == 0:
|
||||||
self.isParent = False
|
self.isParent = False
|
||||||
@ -200,9 +204,9 @@ class ForkedProcess(RemoteEventHandler):
|
|||||||
if 'random' in sys.modules:
|
if 'random' in sys.modules:
|
||||||
sys.modules['random'].seed(os.getpid() ^ int(time.time()*10000%10000))
|
sys.modules['random'].seed(os.getpid() ^ int(time.time()*10000%10000))
|
||||||
|
|
||||||
RemoteEventHandler.__init__(self, remoteConn, name+'_child', pid=os.getppid())
|
#ppid = 0 if not hasattr(os, 'getppid') else os.getppid()
|
||||||
|
RemoteEventHandler.__init__(self, remoteConn, name+'_child', pid=ppid)
|
||||||
|
|
||||||
ppid = os.getppid()
|
|
||||||
self.forkedProxies = {}
|
self.forkedProxies = {}
|
||||||
for name, proxyId in proxyIDs.iteritems():
|
for name, proxyId in proxyIDs.iteritems():
|
||||||
self.forkedProxies[name] = ObjectProxy(ppid, proxyId=proxyId, typeStr=repr(preProxy[name]))
|
self.forkedProxies[name] = ObjectProxy(ppid, proxyId=proxyId, typeStr=repr(preProxy[name]))
|
||||||
@ -318,7 +322,7 @@ class QtProcess(Process):
|
|||||||
except ClosedError:
|
except ClosedError:
|
||||||
self.timer.stop()
|
self.timer.stop()
|
||||||
|
|
||||||
def startQtEventLoop(name, port, authkey):
|
def startQtEventLoop(name, port, authkey, ppid):
|
||||||
conn = multiprocessing.connection.Client(('localhost', int(port)), authkey=authkey)
|
conn = multiprocessing.connection.Client(('localhost', int(port)), authkey=authkey)
|
||||||
from pyqtgraph.Qt import QtGui, QtCore
|
from pyqtgraph.Qt import QtGui, QtCore
|
||||||
#from PyQt4 import QtGui, QtCore
|
#from PyQt4 import QtGui, QtCore
|
||||||
@ -330,7 +334,8 @@ def startQtEventLoop(name, port, authkey):
|
|||||||
## until it is explicitly closed by the parent process.
|
## until it is explicitly closed by the parent process.
|
||||||
|
|
||||||
global HANDLER
|
global HANDLER
|
||||||
HANDLER = RemoteQtEventHandler(conn, name, os.getppid())
|
#ppid = 0 if not hasattr(os, 'getppid') else os.getppid()
|
||||||
|
HANDLER = RemoteQtEventHandler(conn, name, ppid)
|
||||||
HANDLER.startEventTimer()
|
HANDLER.startEventTimer()
|
||||||
app.exec_()
|
app.exec_()
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import pyqtgraph.multiprocess as mp
|
|||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
from .GraphicsView import GraphicsView
|
from .GraphicsView import GraphicsView
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import mmap, tempfile, ctypes, atexit
|
import mmap, tempfile, ctypes, atexit, sys, random
|
||||||
|
|
||||||
__all__ = ['RemoteGraphicsView']
|
__all__ = ['RemoteGraphicsView']
|
||||||
|
|
||||||
@ -30,10 +30,12 @@ class RemoteGraphicsView(QtGui.QWidget):
|
|||||||
self.setFocusPolicy(self._view.focusPolicy())
|
self.setFocusPolicy(self._view.focusPolicy())
|
||||||
self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)
|
self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)
|
||||||
self.setMouseTracking(True)
|
self.setMouseTracking(True)
|
||||||
|
self.shm = None
|
||||||
shmFileName = self._view.shmFileName()
|
shmFileName = self._view.shmFileName()
|
||||||
self.shmFile = open(shmFileName, 'r')
|
if 'win' in sys.platform:
|
||||||
self.shm = mmap.mmap(self.shmFile.fileno(), mmap.PAGESIZE, mmap.MAP_SHARED, mmap.PROT_READ)
|
self.shmtag = shmFileName
|
||||||
|
else:
|
||||||
|
self.shmFile = open(shmFileName, 'r')
|
||||||
|
|
||||||
self._view.sceneRendered.connect(mp.proxy(self.remoteSceneChanged)) #, callSync='off'))
|
self._view.sceneRendered.connect(mp.proxy(self.remoteSceneChanged)) #, callSync='off'))
|
||||||
## Note: we need synchronous signals
|
## Note: we need synchronous signals
|
||||||
@ -53,11 +55,16 @@ class RemoteGraphicsView(QtGui.QWidget):
|
|||||||
return QtCore.QSize(*self._sizeHint)
|
return QtCore.QSize(*self._sizeHint)
|
||||||
|
|
||||||
def remoteSceneChanged(self, data):
|
def remoteSceneChanged(self, data):
|
||||||
w, h, size = data
|
w, h, size, newfile = data
|
||||||
#self._sizeHint = (whint, hhint)
|
#self._sizeHint = (whint, hhint)
|
||||||
if self.shm.size != size:
|
if self.shm is None or self.shm.size != size:
|
||||||
self.shm.close()
|
if self.shm is not None:
|
||||||
self.shm = mmap.mmap(self.shmFile.fileno(), size, mmap.MAP_SHARED, mmap.PROT_READ)
|
self.shm.close()
|
||||||
|
if 'win' in sys.platform:
|
||||||
|
self.shmtag = newfile ## on windows, we create a new tag for every resize
|
||||||
|
self.shm = mmap.mmap(-1, size, self.shmtag) ## can't use tmpfile on windows because the file can only be opened once.
|
||||||
|
else:
|
||||||
|
self.shm = mmap.mmap(self.shmFile.fileno(), size, mmap.MAP_SHARED, mmap.PROT_READ)
|
||||||
self.shm.seek(0)
|
self.shm.seek(0)
|
||||||
self._img = QtGui.QImage(self.shm.read(w*h*4), w, h, QtGui.QImage.Format_ARGB32)
|
self._img = QtGui.QImage(self.shm.read(w*h*4), w, h, QtGui.QImage.Format_ARGB32)
|
||||||
self.update()
|
self.update()
|
||||||
@ -112,13 +119,14 @@ class Renderer(GraphicsView):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwds):
|
def __init__(self, *args, **kwds):
|
||||||
## Create shared memory for rendered image
|
## Create shared memory for rendered image
|
||||||
#fd = os.open('/tmp/mmaptest', os.O_CREAT | os.O_TRUNC | os.O_RDWR)
|
if 'win' in sys.platform:
|
||||||
#os.write(fd, '\x00' * mmap.PAGESIZE)
|
self.shmtag = "pyqtgraph_shmem_" + ''.join([chr((random.getrandbits(20)%25) + 97) for i in range(20)])
|
||||||
self.shmFile = tempfile.NamedTemporaryFile(prefix='pyqtgraph_shmem_')
|
self.shm = mmap.mmap(-1, mmap.PAGESIZE, self.shmtag) # use anonymous mmap on windows
|
||||||
self.shmFile.write('\x00' * mmap.PAGESIZE)
|
else:
|
||||||
#fh.flush()
|
self.shmFile = tempfile.NamedTemporaryFile(prefix='pyqtgraph_shmem_')
|
||||||
fd = self.shmFile.fileno()
|
self.shmFile.write('\x00' * mmap.PAGESIZE)
|
||||||
self.shm = mmap.mmap(fd, mmap.PAGESIZE, mmap.MAP_SHARED, mmap.PROT_WRITE)
|
fd = self.shmFile.fileno()
|
||||||
|
self.shm = mmap.mmap(fd, mmap.PAGESIZE, mmap.MAP_SHARED, mmap.PROT_WRITE)
|
||||||
atexit.register(self.close)
|
atexit.register(self.close)
|
||||||
|
|
||||||
GraphicsView.__init__(self, *args, **kwds)
|
GraphicsView.__init__(self, *args, **kwds)
|
||||||
@ -130,10 +138,14 @@ class Renderer(GraphicsView):
|
|||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
self.shm.close()
|
self.shm.close()
|
||||||
self.shmFile.close()
|
if 'win' not in sys.platform:
|
||||||
|
self.shmFile.close()
|
||||||
|
|
||||||
def shmFileName(self):
|
def shmFileName(self):
|
||||||
return self.shmFile.name
|
if 'win' in sys.platform:
|
||||||
|
return self.shmtag
|
||||||
|
else:
|
||||||
|
return self.shmFile.name
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
self.img = None
|
self.img = None
|
||||||
@ -152,7 +164,14 @@ class Renderer(GraphicsView):
|
|||||||
return
|
return
|
||||||
size = self.width() * self.height() * 4
|
size = self.width() * self.height() * 4
|
||||||
if size > self.shm.size():
|
if size > self.shm.size():
|
||||||
self.shm.resize(size)
|
if 'win' in sys.platform:
|
||||||
|
## windows says "WindowsError: [Error 87] the parameter is incorrect" if we try to resize the mmap
|
||||||
|
self.shm.close()
|
||||||
|
## it also says (sometimes) 'access is denied' if we try to reuse the tag.
|
||||||
|
self.shmtag = "pyqtgraph_shmem_" + ''.join([chr((random.getrandbits(20)%25) + 97) for i in range(20)])
|
||||||
|
self.shm = mmap.mmap(-1, size, self.shmtag)
|
||||||
|
else:
|
||||||
|
self.shm.resize(size)
|
||||||
address = ctypes.addressof(ctypes.c_char.from_buffer(self.shm, 0))
|
address = ctypes.addressof(ctypes.c_char.from_buffer(self.shm, 0))
|
||||||
|
|
||||||
## render the scene directly to shared memory
|
## render the scene directly to shared memory
|
||||||
@ -161,7 +180,7 @@ class Renderer(GraphicsView):
|
|||||||
p = QtGui.QPainter(self.img)
|
p = QtGui.QPainter(self.img)
|
||||||
self.render(p, self.viewRect(), self.rect())
|
self.render(p, self.viewRect(), self.rect())
|
||||||
p.end()
|
p.end()
|
||||||
self.sceneRendered.emit((self.width(), self.height(), self.shm.size()))
|
self.sceneRendered.emit((self.width(), self.height(), self.shm.size(), self.shmFileName()))
|
||||||
|
|
||||||
def mousePressEvent(self, typ, pos, gpos, btn, btns, mods):
|
def mousePressEvent(self, typ, pos, gpos, btn, btns, mods):
|
||||||
typ = QtCore.QEvent.Type(typ)
|
typ = QtCore.QEvent.Type(typ)
|
||||||
@ -202,4 +221,4 @@ class Renderer(GraphicsView):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user