This commit is contained in:
Luke Campagnola 2013-02-13 11:43:22 -05:00
parent 4dbc411d19
commit ccc81c6919
4 changed files with 114 additions and 34 deletions

View File

@ -5,20 +5,25 @@ if __name__ == '__main__':
if hasattr(os, 'setpgrp'):
os.setpgrp() ## prevents signals (notably keyboard interrupt) being forwarded from parent to this process
if sys.version[0] == '3':
name, port, authkey, ppid, targetStr, path, pyside = pickle.load(sys.stdin.buffer)
#name, port, authkey, ppid, targetStr, path, pyside = pickle.load(sys.stdin.buffer)
opts = pickle.load(sys.stdin.buffer)
else:
name, port, authkey, ppid, targetStr, path, pyside = pickle.load(sys.stdin)
#name, port, authkey, ppid, targetStr, path, pyside = pickle.load(sys.stdin)
opts = pickle.load(sys.stdin)
#print "key:", ' '.join([str(ord(x)) for x in authkey])
path = opts.pop('path', 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.
while len(sys.path) > 0:
sys.path.pop()
sys.path.extend(path)
if pyside:
if opts.pop('pyside', False):
import PySide
#import pyqtgraph
#import pyqtgraph.multiprocess.processes
targetStr = opts.pop('targetStr')
target = pickle.loads(targetStr) ## unpickling the target should import everything we need
target(name, port, authkey, ppid)
#target(name, port, authkey, ppid)
target(**opts) ## Send all other options to the target function
sys.exit(0)

View File

@ -35,7 +35,7 @@ class Process(RemoteEventHandler):
ProxyObject for more information.
"""
def __init__(self, name=None, target=None, executable=None, copySysPath=True):
def __init__(self, name=None, target=None, executable=None, copySysPath=True, debug=False):
"""
============ =============================================================
Arguments:
@ -46,7 +46,9 @@ class Process(RemoteEventHandler):
process to process requests from the parent process until it
is asked to quit. If you wish to specify a different target,
it must be picklable (bound methods are not).
copySysPath If true, copy the contents of sys.path to the remote process
copySysPath If True, copy the contents of sys.path to the remote process
debug If True, print detailed information about communication
with the child process.
============ =============================================================
"""
@ -56,6 +58,7 @@ class Process(RemoteEventHandler):
name = str(self)
if executable is None:
executable = sys.executable
self.debug = debug
## random authentication key
authkey = os.urandom(20)
@ -75,23 +78,46 @@ class Process(RemoteEventHandler):
## start remote process, instruct it to run target function
sysPath = sys.path if copySysPath else None
bootstrap = os.path.abspath(os.path.join(os.path.dirname(__file__), 'bootstrap.py'))
self.debugMsg('Starting child process (%s %s)' % (executable, bootstrap))
self.proc = subprocess.Popen((executable, bootstrap), stdin=subprocess.PIPE)
targetStr = pickle.dumps(target) ## double-pickle target so that child has a chance to
## set its sys.path properly before unpickling the target
pid = os.getpid() # we must sent pid to child because windows does not have getppid
pid = os.getpid() # we must send pid to child because windows does not have getppid
pyside = USE_PYSIDE
## Send everything the remote process needs to start correctly
pickle.dump((name+'_child', port, authkey, pid, targetStr, sysPath, pyside), self.proc.stdin)
data = dict(
name=name+'_child',
port=port,
authkey=authkey,
ppid=pid,
targetStr=targetStr,
path=sysPath,
pyside=pyside,
debug=debug
)
pickle.dump(data, self.proc.stdin)
self.proc.stdin.close()
## open connection for remote process
conn = l.accept()
RemoteEventHandler.__init__(self, conn, name+'_parent', pid=self.proc.pid)
self.debugMsg('Listening for child process..')
while True:
try:
conn = l.accept()
break
except IOError as err:
if err.errno == 4: # interrupted; try again
continue
else:
raise
RemoteEventHandler.__init__(self, conn, name+'_parent', pid=self.proc.pid, debug=debug)
self.debugMsg('Connected to child process.')
atexit.register(self.join)
def join(self, timeout=10):
self.debugMsg('Joining child process..')
if self.proc.poll() is None:
self.close()
start = time.time()
@ -99,13 +125,14 @@ class Process(RemoteEventHandler):
if timeout is not None and time.time() - start > timeout:
raise Exception('Timed out waiting for remote process to end.')
time.sleep(0.05)
self.debugMsg('Child process exited. (%d)' % self.proc.returncode)
def startEventLoop(name, port, authkey, ppid):
def startEventLoop(name, port, authkey, ppid, debug=False):
conn = multiprocessing.connection.Client(('localhost', int(port)), authkey=authkey)
global HANDLER
#ppid = 0 if not hasattr(os, 'getppid') else os.getppid()
HANDLER = RemoteEventHandler(conn, name, ppid)
HANDLER = RemoteEventHandler(conn, name, ppid, debug=debug)
while True:
try:
HANDLER.processRequests() # exception raised when the loop should exit
@ -329,7 +356,7 @@ class QtProcess(Process):
except ClosedError:
self.timer.stop()
def startQtEventLoop(name, port, authkey, ppid):
def startQtEventLoop(name, port, authkey, ppid, debug=False):
conn = multiprocessing.connection.Client(('localhost', int(port)), authkey=authkey)
from pyqtgraph.Qt import QtGui, QtCore
#from PyQt4 import QtGui, QtCore
@ -342,7 +369,7 @@ def startQtEventLoop(name, port, authkey, ppid):
global HANDLER
#ppid = 0 if not hasattr(os, 'getppid') else os.getppid()
HANDLER = RemoteQtEventHandler(conn, name, ppid)
HANDLER = RemoteQtEventHandler(conn, name, ppid, debug=debug)
HANDLER.startEventTimer()
app.exec_()

View File

@ -42,7 +42,8 @@ class RemoteEventHandler(object):
handlers = {} ## maps {process ID : handler}. This allows unpickler to determine which process
## an object proxy belongs to
def __init__(self, connection, name, pid):
def __init__(self, connection, name, pid, debug=False):
self.debug = debug
self.conn = connection
self.name = name
self.results = {} ## reqId: (status, result); cache of request results received from the remote process
@ -76,6 +77,11 @@ class RemoteEventHandler(object):
print(pid, cls.handlers)
raise
def debugMsg(self, msg):
if not self.debug:
return
print("[%d] %s" % (os.getpid(), str(msg)))
def getProxyOption(self, opt):
return self.proxyOptions[opt]
@ -91,7 +97,9 @@ class RemoteEventHandler(object):
after no more events are immediately available. (non-blocking)
Returns the number of events processed.
"""
self.debugMsg('processRequests:')
if self.exited:
self.debugMsg(' processRequests: exited already; raise ClosedError.')
raise ClosedError()
numProcessed = 0
@ -100,37 +108,64 @@ class RemoteEventHandler(object):
self.handleRequest()
numProcessed += 1
except ClosedError:
self.debugMsg(' processRequests: got ClosedError from handleRequest; setting exited=True.')
self.exited = True
raise
except IOError as err:
if err.errno == 4: ## interrupted system call; try again
continue
else:
raise
#except IOError as err: ## let handleRequest take care of this.
#self.debugMsg(' got IOError from handleRequest; try again.')
#if err.errno == 4: ## interrupted system call; try again
#continue
#else:
#raise
except:
print("Error in process %s" % self.name)
sys.excepthook(*sys.exc_info())
self.debugMsg(' processRequests: finished %d requests' % numProcessed)
return numProcessed
def handleRequest(self):
"""Handle a single request from the remote process.
Blocks until a request is available."""
result = None
try:
cmd, reqId, nByteMsgs, optStr = self.conn.recv() ## args, kwds are double-pickled to ensure this recv() call never fails
except (EOFError, IOError):
## remote process has shut down; end event loop
raise ClosedError()
#print os.getpid(), "received request:", cmd, reqId
while True:
try:
## args, kwds are double-pickled to ensure this recv() call never fails
cmd, reqId, nByteMsgs, optStr = self.conn.recv()
break
except EOFError:
self.debugMsg(' handleRequest: got EOFError from recv; raise ClosedError.')
## remote process has shut down; end event loop
raise ClosedError()
except IOError as err:
if err.errno == 4: ## interrupted system call; try again
self.debugMsg(' handleRequest: got IOError 4 from recv; try again.')
continue
else:
self.debugMsg(' handleRequest: got IOError %d from recv (%s); raise ClosedError.' % (err.errno, err.strerror))
raise ClosedError()
self.debugMsg(" handleRequest: received %s %s" % (str(cmd), str(reqId)))
## read byte messages following the main request
byteData = []
if nByteMsgs > 0:
self.debugMsg(" handleRequest: reading %d byte messages" % nByteMsgs)
for i in range(nByteMsgs):
try:
byteData.append(self.conn.recv_bytes())
except (EOFError, IOError):
raise ClosedError()
while True:
try:
byteData.append(self.conn.recv_bytes())
break
except EOFError:
self.debugMsg(" handleRequest: got EOF while reading byte messages; raise ClosedError.")
raise ClosedError()
except IOError as err:
if err.errno == 4:
self.debugMsg(" handleRequest: got IOError 4 while reading byte messages; try again.")
continue
else:
self.debugMsg(" handleRequest: got IOError while reading byte messages; raise ClosedError.")
raise ClosedError()
try:
@ -140,6 +175,7 @@ class RemoteEventHandler(object):
## (this is already a return from a previous request)
opts = pickle.loads(optStr)
self.debugMsg(" handleRequest: id=%s opts=%s" % (str(reqId), str(opts)))
#print os.getpid(), "received request:", cmd, reqId, opts
returnType = opts.get('returnType', 'auto')
@ -213,6 +249,7 @@ class RemoteEventHandler(object):
if reqId is not None:
if exc is None:
self.debugMsg(" handleRequest: sending return value for %d: %s" % (reqId, str(result)))
#print "returnValue:", returnValue, result
if returnType == 'auto':
result = self.autoProxy(result, self.proxyOptions['noProxyTypes'])
@ -225,6 +262,7 @@ class RemoteEventHandler(object):
sys.excepthook(*sys.exc_info())
self.replyError(reqId, *sys.exc_info())
else:
self.debugMsg(" handleRequest: returning exception for %d" % reqId)
self.replyError(reqId, *exc)
elif exc is not None:
@ -368,13 +406,16 @@ class RemoteEventHandler(object):
## Send primary request
request = (request, reqId, nByteMsgs, optStr)
self.debugMsg('send request: cmd=%s nByteMsgs=%d id=%s opts=%s' % (str(request[0]), nByteMsgs, str(reqId), str(opts)))
self.conn.send(request)
## follow up by sending byte messages
if byteData is not None:
for obj in byteData: ## Remote process _must_ be prepared to read the same number of byte messages!
self.conn.send_bytes(obj)
self.debugMsg(' sent %d byte messages' % len(byteData))
self.debugMsg(' call sync: %s' % callSync)
if callSync == 'off':
return

View File

@ -1,4 +1,4 @@
from pyqtgraph.Qt import QtGui, QtCore
from pyqtgraph.Qt import QtGui, QtCore, USE_PYSIDE
import pyqtgraph.multiprocess as mp
import pyqtgraph as pg
from .GraphicsView import GraphicsView
@ -21,13 +21,14 @@ class RemoteGraphicsView(QtGui.QWidget):
self._sizeHint = (640,480) ## no clue why this is needed, but it seems to be the default sizeHint for GraphicsView.
## without it, the widget will not compete for space against another GraphicsView.
QtGui.QWidget.__init__(self)
self._proc = mp.QtProcess()
self._proc = mp.QtProcess(debug=False)
self.pg = self._proc._import('pyqtgraph')
self.pg.setConfigOptions(**self.pg.CONFIG_OPTIONS)
rpgRemote = self._proc._import('pyqtgraph.widgets.RemoteGraphicsView')
self._view = rpgRemote.Renderer(*args, **kwds)
self._view._setProxyOptions(deferGetattr=True)
self.setFocusPolicy(QtCore.Qt.FocusPolicy(self._view.focusPolicy()))
self.setFocusPolicy(QtCore.Qt.StrongFocus)
self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)
self.setMouseTracking(True)
self.shm = None
@ -114,6 +115,7 @@ class RemoteGraphicsView(QtGui.QWidget):
return self._proc
class Renderer(GraphicsView):
## Created by the remote process to handle render requests
sceneRendered = QtCore.Signal(object)
@ -175,7 +177,12 @@ class Renderer(GraphicsView):
address = ctypes.addressof(ctypes.c_char.from_buffer(self.shm, 0))
## render the scene directly to shared memory
self.img = QtGui.QImage(address, self.width(), self.height(), QtGui.QImage.Format_ARGB32)
if USE_PYSIDE:
ch = ctypes.c_char.from_buffer(self.shm, 0)
#ch = ctypes.c_char_p(address)
self.img = QtGui.QImage(ch, self.width(), self.height(), QtGui.QImage.Format_ARGB32)
else:
self.img = QtGui.QImage(address, self.width(), self.height(), QtGui.QImage.Format_ARGB32)
self.img.fill(0xffffffff)
p = QtGui.QPainter(self.img)
self.render(p, self.viewRect(), self.rect())