eric6/Utilities/BackgroundService.py

changeset 6942
2602857055c5
parent 6645
ad476851d7e0
child 7192
a22eee00b052
equal deleted inserted replaced
6941:f99d60d6b59b 6942:2602857055c5
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2013 - 2019 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5 # pylint: disable=C0103
6
7 """
8 Module implementing a background service for the various checkers and other
9 python interpreter dependent functions.
10 """
11
12 from __future__ import unicode_literals
13
14 import json
15 import os
16 import struct
17 import sys
18 from zlib import adler32
19
20 from PyQt5.QtCore import QProcess, pyqtSignal, QTimer
21 from PyQt5.QtWidgets import QApplication
22 from PyQt5.QtNetwork import QTcpServer, QHostAddress
23
24 from E5Gui import E5MessageBox
25 from E5Gui.E5Application import e5App
26 import Preferences
27 import Utilities
28
29 from eric6config import getConfig
30
31
32 class BackgroundService(QTcpServer):
33 """
34 Class implementing the main part of the background service.
35
36 @signal serviceNotAvailable(function, language, filename, message)
37 emitted to indicate the non-availability of a service function
38 (str, str, str, str)
39 @signal batchJobDone(function, language) emitted to indicate the end of
40 a batch job (str, str)
41 """
42 serviceNotAvailable = pyqtSignal(str, str, str, str)
43 batchJobDone = pyqtSignal(str, str)
44
45 def __init__(self):
46 """
47 Constructor of the BackgroundService class.
48 """
49 self.processes = {}
50 self.connections = {}
51 self.isWorking = None
52 self.runningJob = [None, None, None, None]
53 self.__queue = []
54 self.services = {}
55
56 super(BackgroundService, self).__init__()
57
58 networkInterface = Preferences.getDebugger("NetworkInterface")
59 if networkInterface == "all" or '.' in networkInterface:
60 self.hostAddress = '127.0.0.1'
61 else:
62 self.hostAddress = '::1'
63 self.listen(QHostAddress(self.hostAddress))
64
65 self.newConnection.connect(self.on_newConnection)
66
67 port = self.serverPort()
68 ## Note: Need the port if started external in debugger:
69 print('BackgroundService listening on: {0:d}'.format(port))
70 # __IGNORE_WARNING__
71 for pyName in ['Python2', 'Python3']:
72 venvName = Preferences.getDebugger(
73 pyName + "VirtualEnv")
74 interpreter = e5App().getObject("VirtualEnvManager")\
75 .getVirtualenvInterpreter(venvName)
76 if not interpreter and int(pyName[-1]) == sys.version_info[0]:
77 interpreter = sys.executable.replace("w.exe", ".exe")
78 if interpreter:
79 process = self.__startExternalClient(interpreter, port)
80 if process:
81 self.processes[pyName] = process, interpreter
82
83 def __startExternalClient(self, interpreter, port):
84 """
85 Private method to start the background client as external process.
86
87 @param interpreter path and name of the executable to start (string)
88 @param port socket port to which the interpreter should connect (int)
89 @return the process object (QProcess or None)
90 """
91 if interpreter == "" or not Utilities.isinpath(interpreter):
92 return None
93
94 backgroundClient = os.path.join(
95 getConfig('ericDir'),
96 "Utilities", "BackgroundClient.py")
97 proc = QProcess()
98 proc.setProcessChannelMode(QProcess.ForwardedChannels)
99 args = [backgroundClient, self.hostAddress, str(port),
100 str(Preferences.getUI("BackgroundServiceProcesses"))]
101 proc.start(interpreter, args)
102 if not proc.waitForStarted(10000):
103 proc = None
104 return proc
105
106 def __processQueue(self):
107 """
108 Private method to take the next service request and send it to the
109 client.
110 """
111 if self.__queue and self.isWorking is None:
112 fx, lang, fn, data = self.__queue.pop(0)
113 self.isWorking = lang
114 self.runningJob = fx, lang, fn, data
115 self.__send(fx, lang, fn, data)
116
117 def __send(self, fx, lang, fn, data):
118 """
119 Private method to send a job request to one of the clients.
120
121 @param fx remote function name to execute (str)
122 @param lang language to connect to (str)
123 @param fn filename for identification (str)
124 @param data function argument(s) (any basic datatype)
125 """
126 self.__cancelled = False
127 connection = self.connections.get(lang)
128 if connection is None:
129 if fx != 'INIT':
130 # Avoid growing recursion deep which could itself result in an
131 # exception
132 QTimer.singleShot(
133 0,
134 lambda: self.serviceNotAvailable.emit(
135 fx, lang, fn, self.tr(
136 '{0} not configured.').format(lang)))
137 # Reset flag and continue processing queue
138 self.isWorking = None
139 self.__processQueue()
140 else:
141 packedData = json.dumps([fx, fn, data])
142 if sys.version_info[0] == 3:
143 packedData = bytes(packedData, 'utf-8')
144 header = struct.pack(
145 b'!II', len(packedData), adler32(packedData) & 0xffffffff)
146 connection.write(header)
147 connection.write(b'JOB ') # 6 character message type
148 connection.write(packedData)
149
150 def __receive(self, lang):
151 """
152 Private method to receive the response from the clients.
153
154 @param lang language of the incomming connection (str)
155 """
156 connection = self.connections[lang]
157 while connection.bytesAvailable():
158 if self.__cancelled:
159 connection.readAll()
160 continue
161
162 header = connection.read(struct.calcsize(b'!II'))
163 length, datahash = struct.unpack(b'!II', header)
164
165 packedData = b''
166 while len(packedData) < length:
167 maxSize = length - len(packedData)
168 if connection.bytesAvailable() < maxSize:
169 connection.waitForReadyRead(50)
170 packedData += connection.read(maxSize)
171
172 assert adler32(packedData) & 0xffffffff == datahash, \
173 'Hashes not equal'
174 if sys.version_info[0] == 3:
175 packedData = packedData.decode('utf-8')
176 # "check" if is's a tuple of 3 values
177 fx, fn, data = json.loads(packedData)
178
179 if fx == 'INIT':
180 pass
181 elif fx == 'EXCEPTION':
182 # Remove connection because it'll close anyway
183 self.connections.pop(lang, None)
184 # Call sys.excepthook(type, value, traceback) to emulate the
185 # exception which was caught on the client
186 sys.excepthook(*data)
187 res = E5MessageBox.question(
188 None,
189 self.tr("Restart background client?"),
190 self.tr(
191 "<p>The background client for <b>{0}</b> has stopped"
192 " due to an exception. It's used by various plug-ins"
193 " like the different checkers.</p>"
194 "<p>Select"
195 "<ul>"
196 "<li><b>'Yes'</b> to restart the client, but abort the"
197 " last job</li>"
198 "<li><b>'Retry'</b> to restart the client and the last"
199 " job</li>"
200 "<li><b>'No'</b> to leave the client off.</li>"
201 "</ul></p>"
202 "<p>Note: The client can be restarted by opening and"
203 " accepting the preferences dialog or reloading/"
204 "changing the project.</p>").format(lang),
205 E5MessageBox.Yes | E5MessageBox.No | E5MessageBox.Retry,
206 E5MessageBox.Yes)
207
208 if res == E5MessageBox.Retry:
209 self.enqueueRequest(*self.runningJob)
210 else:
211 fx, lng, fn, data = self.runningJob
212 try:
213 self.services[(fx, lng)][3](fx, lng, fn, self.tr(
214 'An error in Erics background client stopped the'
215 ' service.')
216 )
217 except (KeyError, TypeError):
218 # ignore silently
219 pass
220 if res != E5MessageBox.No:
221 self.isWorking = None
222 self.restartService(lang, True)
223 return
224 elif data == 'Unknown service.':
225 callback = self.services.get((fx, lang))
226 if callback:
227 callback[3](fx, lang, fn, data)
228 elif fx.startswith("batch_"):
229 fx = fx.replace("batch_", "")
230 if data != "__DONE__":
231 callback = self.services.get((fx, lang))
232 if callback:
233 if isinstance(data, (list, tuple)):
234 callback[2](fn, *data)
235 elif isinstance(data, str):
236 callback[3](fx, lang, fn, data)
237 continue
238 else:
239 self.batchJobDone.emit(fx, lang)
240 else:
241 callback = self.services.get((fx, lang))
242 if callback:
243 callback[2](fn, *data)
244
245 self.isWorking = None
246 self.__processQueue()
247
248 def preferencesOrProjectChanged(self):
249 """
250 Public slot to restart the built in languages.
251 """
252 for pyName in ['Python2', 'Python3']:
253 venvName = Preferences.getDebugger(
254 pyName + "VirtualEnv")
255 interpreter = e5App().getObject("VirtualEnvManager")\
256 .getVirtualenvInterpreter(venvName)
257 if not interpreter and int(pyName[-1]) == sys.version_info[0]:
258 interpreter = sys.executable.replace("w.exe", ".exe")
259
260 # Tweak the processes list to reflect the changed interpreter
261 proc, inter = self.processes.pop(pyName, [None, None])
262 self.processes[pyName] = proc, interpreter
263
264 self.restartService(pyName)
265
266 def restartService(self, language, forceKill=False):
267 """
268 Public method to restart a given lanuage.
269
270 @param language to restart (str)
271 @keyparam forceKill flag to kill a running task (bool)
272 """
273 try:
274 proc, interpreter = self.processes.pop(language)
275 except KeyError:
276 return
277
278 # Don't kill a process if it's still working
279 if not forceKill:
280 while self.isWorking is not None:
281 QApplication.processEvents()
282
283 conn = self.connections.pop(language, None)
284 if conn:
285 conn.blockSignals(True)
286 conn.close()
287 if proc:
288 proc.close()
289
290 if interpreter:
291 port = self.serverPort()
292 process = self.__startExternalClient(interpreter, port)
293 if process:
294 self.processes[language] = process, interpreter
295
296 def enqueueRequest(self, fx, lang, fn, data):
297 """
298 Public method implementing a queued processing of incomming events.
299
300 Dublicate service requests updates an older request to avoid overrun or
301 starving of the services.
302 @param fx function name of the service (str)
303 @param lang language to connect to (str)
304 @param fn filename for identification (str)
305 @param data function argument(s) (any basic datatype(s))
306 """
307 args = [fx, lang, fn, data]
308 if fx == 'INIT':
309 self.__queue.insert(0, args)
310 else:
311 for pendingArg in self.__queue:
312 # Check if it's the same service request (fx, lang, fn equal)
313 if pendingArg[:3] == args[:3]:
314 # Update the data
315 pendingArg[3] = args[3]
316 break
317 else:
318 self.__queue.append(args)
319 self.__processQueue()
320
321 def requestCancel(self, fx, lang):
322 """
323 Public method to ask a batch job to terminate.
324
325 @param fx function name of the service (str)
326 @param lang language to connect to (str)
327 """
328 self.__cancelled = True
329
330 entriesToRemove = []
331 for pendingArg in self.__queue:
332 if pendingArg[:2] == [fx, lang]:
333 entriesToRemove.append(pendingArg)
334 for entryToRemove in entriesToRemove:
335 self.__queue.remove(entryToRemove)
336
337 connection = self.connections.get(lang)
338 if connection is None:
339 return
340 else:
341 header = struct.pack(b'!II', 0, 0)
342 connection.write(header)
343 connection.write(b'CANCEL') # 6 character message type
344
345 def serviceConnect(
346 self, fx, lang, modulepath, module, callback,
347 onErrorCallback=None, onBatchDone=None):
348 """
349 Public method to announce a new service to the background
350 service/client.
351
352 @param fx function name of the service (str)
353 @param lang language of the new service (str)
354 @param modulepath full path to the module (str)
355 @param module name to import (str)
356 @param callback function called on service response (function)
357 @param onErrorCallback function called, if client isn't available
358 (function)
359 @param onBatchDone function called when a batch job is done (function)
360 """
361 self.services[(fx, lang)] = \
362 modulepath, module, callback, onErrorCallback
363 self.enqueueRequest('INIT', lang, fx, [modulepath, module])
364 if onErrorCallback:
365 self.serviceNotAvailable.connect(onErrorCallback)
366 if onBatchDone:
367 self.batchJobDone.connect(onBatchDone)
368
369 def serviceDisconnect(self, fx, lang):
370 """
371 Public method to remove the service from the service list.
372
373 @param fx function name of the service (function)
374 @param lang language of the service (str)
375 """
376 serviceArgs = self.services.pop((fx, lang), None)
377 if serviceArgs and serviceArgs[3]:
378 self.serviceNotAvailable.disconnect(serviceArgs[3])
379
380 def on_newConnection(self):
381 """
382 Private slot for new incomming connections from the clients.
383 """
384 connection = self.nextPendingConnection()
385 if not connection.waitForReadyRead(1000):
386 return
387 lang = connection.read(64)
388 if sys.version_info[0] == 3:
389 lang = lang.decode('utf-8')
390 # Avoid hanging of eric on shutdown
391 if self.connections.get(lang):
392 self.connections[lang].close()
393 if self.isWorking == lang:
394 self.isWorking = None
395 self.connections[lang] = connection
396 connection.readyRead.connect(
397 lambda x=lang: self.__receive(x))
398 connection.disconnected.connect(
399 lambda x=lang: self.on_disconnectSocket(x))
400
401 for (fx, lng), args in self.services.items():
402 if lng == lang:
403 # Register service with modulepath and module
404 self.enqueueRequest('INIT', lng, fx, args[:2])
405
406 # Syntax check the open editors again
407 try:
408 vm = e5App().getObject("ViewManager")
409 except KeyError:
410 return
411 for editor in vm.getOpenEditors():
412 if editor.getLanguage() == lang:
413 QTimer.singleShot(0, editor.checkSyntax)
414
415 def on_disconnectSocket(self, lang):
416 """
417 Private slot called when connection to a client is lost.
418
419 @param lang client language which connection is lost (str)
420 """
421 conn = self.connections.pop(lang, None)
422 if conn:
423 conn.close()
424 fx, lng, fn, data = self.runningJob
425 if fx != 'INIT' and lng == lang:
426 self.services[(fx, lng)][3](fx, lng, fn, self.tr(
427 "Eric's background client disconnected because of an"
428 " unknown reason.")
429 )
430 self.isWorking = None
431
432 res = E5MessageBox.yesNo(
433 None,
434 self.tr('Background client disconnected.'),
435 self.tr(
436 'The background client for <b>{0}</b> disconnected because'
437 ' of an unknown reason.<br>Should it be restarted?'
438 ).format(lang),
439 yesDefault=True)
440 if res:
441 self.restartService(lang)
442
443 def shutdown(self):
444 """
445 Public method to cleanup the connections and processes when eric is
446 shutting down.
447 """
448 self.close()
449
450 for connection in self.connections.values():
451 # Prevent calling of on_disconnectSocket
452 connection.blockSignals(True)
453 connection.close()
454
455 for process, _interpreter in self.processes.values():
456 process.close()
457 process = None

eric ide

mercurial