1# -*- coding: utf-8 -*- 2# 3# Copyright © Spyder Project Contributors 4# Licensed under the terms of the MIT License 5# (see spyder/__init__.py for details) 6 7# Local imports 8import imp 9import os 10import os.path as osp 11import sys 12import uuid 13 14# Third party imports 15from qtpy.QtCore import (QObject, QProcess, QProcessEnvironment, 16 QSocketNotifier, QTimer, Signal) 17from qtpy.QtWidgets import QApplication 18import zmq 19 20# Local imports 21from spyder.config.base import debug_print, get_module_path 22 23 24# Heartbeat timer in milliseconds 25HEARTBEAT = 1000 26 27 28class AsyncClient(QObject): 29 30 """ 31 A class which handles a connection to a client through a QProcess. 32 """ 33 34 # Emitted when the client has initialized. 35 initialized = Signal() 36 37 # Emitted when the client errors. 38 errored = Signal() 39 40 # Emitted when a request response is received. 41 received = Signal(object) 42 43 def __init__(self, target, executable=None, name=None, 44 extra_args=None, libs=None, cwd=None, env=None): 45 super(AsyncClient, self).__init__() 46 self.executable = executable or sys.executable 47 self.extra_args = extra_args 48 self.target = target 49 self.name = name or self 50 self.libs = libs 51 self.cwd = cwd 52 self.env = env 53 self.is_initialized = False 54 self.closing = False 55 self.notifier = None 56 self.process = None 57 self.context = zmq.Context() 58 QApplication.instance().aboutToQuit.connect(self.close) 59 60 # Set up the heartbeat timer. 61 self.timer = QTimer(self) 62 self.timer.timeout.connect(self._heartbeat) 63 64 def run(self): 65 """Handle the connection with the server. 66 """ 67 # Set up the zmq port. 68 self.socket = self.context.socket(zmq.PAIR) 69 self.port = self.socket.bind_to_random_port('tcp://*') 70 71 # Set up the process. 72 self.process = QProcess(self) 73 if self.cwd: 74 self.process.setWorkingDirectory(self.cwd) 75 p_args = ['-u', self.target, str(self.port)] 76 if self.extra_args is not None: 77 p_args += self.extra_args 78 79 # Set up environment variables. 80 processEnvironment = QProcessEnvironment() 81 env = self.process.systemEnvironment() 82 if (self.env and 'PYTHONPATH' not in self.env) or self.env is None: 83 python_path = osp.dirname(get_module_path('spyder')) 84 # Add the libs to the python path. 85 for lib in self.libs: 86 try: 87 path = osp.dirname(imp.find_module(lib)[1]) 88 python_path = osp.pathsep.join([python_path, path]) 89 except ImportError: 90 pass 91 env.append("PYTHONPATH=%s" % python_path) 92 if self.env: 93 env.update(self.env) 94 for envItem in env: 95 envName, separator, envValue = envItem.partition('=') 96 processEnvironment.insert(envName, envValue) 97 self.process.setProcessEnvironment(processEnvironment) 98 99 # Start the process and wait for started. 100 self.process.start(self.executable, p_args) 101 self.process.finished.connect(self._on_finished) 102 running = self.process.waitForStarted() 103 if not running: 104 raise IOError('Could not start %s' % self) 105 106 # Set up the socket notifer. 107 fid = self.socket.getsockopt(zmq.FD) 108 self.notifier = QSocketNotifier(fid, QSocketNotifier.Read, self) 109 self.notifier.activated.connect(self._on_msg_received) 110 111 def request(self, func_name, *args, **kwargs): 112 """Send a request to the server. 113 114 The response will be a dictionary the 'request_id' and the 115 'func_name' as well as a 'result' field with the object returned by 116 the function call or or an 'error' field with a traceback. 117 """ 118 if not self.is_initialized: 119 return 120 request_id = uuid.uuid4().hex 121 request = dict(func_name=func_name, 122 args=args, 123 kwargs=kwargs, 124 request_id=request_id) 125 self._send(request) 126 return request_id 127 128 def close(self): 129 """Cleanly close the connection to the server. 130 """ 131 self.closing = True 132 self.is_initialized = False 133 self.timer.stop() 134 135 if self.notifier is not None: 136 self.notifier.activated.disconnect(self._on_msg_received) 137 self.notifier.setEnabled(False) 138 self.notifier = None 139 140 self.request('server_quit') 141 142 if self.process is not None: 143 self.process.waitForFinished(1000) 144 self.process.close() 145 self.context.destroy() 146 147 def _on_finished(self): 148 """Handle a finished signal from the process. 149 """ 150 if self.closing: 151 return 152 if self.is_initialized: 153 debug_print('Restarting %s' % self.name) 154 debug_print(self.process.readAllStandardOutput()) 155 debug_print(self.process.readAllStandardError()) 156 self.is_initialized = False 157 self.notifier.setEnabled(False) 158 self.run() 159 else: 160 debug_print('Errored %s' % self.name) 161 debug_print(self.process.readAllStandardOutput()) 162 debug_print(self.process.readAllStandardError()) 163 self.errored.emit() 164 165 def _on_msg_received(self): 166 """Handle a message trigger from the socket. 167 """ 168 self.notifier.setEnabled(False) 169 while 1: 170 try: 171 resp = self.socket.recv_pyobj(flags=zmq.NOBLOCK) 172 except zmq.ZMQError: 173 self.notifier.setEnabled(True) 174 return 175 if not self.is_initialized: 176 self.is_initialized = True 177 debug_print('Initialized %s' % self.name) 178 self.initialized.emit() 179 self.timer.start(HEARTBEAT) 180 continue 181 resp['name'] = self.name 182 self.received.emit(resp) 183 184 def _heartbeat(self): 185 """Send a heartbeat to keep the server alive. 186 """ 187 self._send(dict(func_name='server_heartbeat')) 188 189 def _send(self, obj): 190 """Send an object to the server. 191 """ 192 try: 193 self.socket.send_pyobj(obj, zmq.NOBLOCK) 194 except Exception as e: 195 debug_print(e) 196 self.is_initialized = False 197 self._on_finished() 198 199 200class PluginClient(AsyncClient): 201 202 def __init__(self, plugin_name, executable=None, env=None, 203 extra_path=None): 204 cwd = os.path.dirname(__file__) 205 super(PluginClient, self).__init__( 206 'plugin_server.py', 207 executable=executable, cwd=cwd, env=env, 208 extra_args=[plugin_name], libs=[plugin_name]) 209 self.name = plugin_name 210 211 212if __name__ == '__main__': 213 app = QApplication(sys.argv) 214 plugin = PluginClient('jedi') 215 plugin.run() 216 217 def handle_return(value): 218 print(value) # spyder: test-skip 219 if value['func_name'] == 'foo': 220 app.quit() 221 else: 222 plugin.request('foo') 223 224 def handle_errored(): 225 print('errored') # spyder: test-skip 226 sys.exit(1) 227 228 def start(): 229 print('start') # spyder: test-skip 230 plugin.request('validate') 231 232 plugin.errored.connect(handle_errored) 233 plugin.received.connect(handle_return) 234 plugin.initialized.connect(start) 235 236 app.exec_() 237