eric7/E5Network/E5JsonServer.py

branch
eric7
changeset 8312
800c432b34c8
parent 8303
0cbba94590d2
child 8318
962bce857696
equal deleted inserted replaced
8311:4e8b98454baa 8312:800c432b34c8
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2017 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the JSON based server base class.
8 """
9
10 import contextlib
11 import json
12
13 from PyQt5.QtCore import (
14 pyqtSlot, QProcess, QProcessEnvironment, QCoreApplication, QEventLoop,
15 QTimer
16 )
17 from PyQt5.QtNetwork import QTcpServer, QHostAddress
18
19 from E5Gui import E5MessageBox
20
21 import Preferences
22 import Utilities
23
24
25 class E5JsonServer(QTcpServer):
26 """
27 Class implementing a JSON based server base class.
28 """
29 def __init__(self, name="", multiplex=False, parent=None):
30 """
31 Constructor
32
33 @param name name of the server (used for output only)
34 @type str
35 @param multiplex flag indicating a multiplexing server
36 @type bool
37 @param parent parent object
38 @type QObject
39 """
40 super().__init__(parent)
41
42 self.__name = name
43 self.__multiplex = multiplex
44 if self.__multiplex:
45 self.__clientProcesses = {}
46 self.__connections = {}
47 else:
48 self.__clientProcess = None
49 self.__connection = None
50
51 # setup the network interface
52 networkInterface = Preferences.getDebugger("NetworkInterface")
53 if networkInterface == "all" or '.' in networkInterface:
54 # IPv4
55 self.__hostAddress = '127.0.0.1'
56 else:
57 # IPv6
58 self.__hostAddress = '::1'
59 self.listen(QHostAddress(self.__hostAddress))
60
61 self.newConnection.connect(self.handleNewConnection)
62
63 port = self.serverPort()
64 ## Note: Need the port if client is started external in debugger.
65 print('JSON server ({1}) listening on: {0:d}' # __IGNORE_WARNING__
66 .format(port, self.__name))
67
68 @pyqtSlot()
69 def handleNewConnection(self):
70 """
71 Public slot for new incoming connections from a client.
72 """
73 connection = self.nextPendingConnection()
74 if not connection.isValid():
75 return
76
77 if self.__multiplex:
78 if not connection.waitForReadyRead(3000):
79 return
80 idString = bytes(connection.readLine()).decode(
81 "utf-8", 'backslashreplace').strip()
82 if idString in self.__connections:
83 self.__connections[idString].close()
84 self.__connections[idString] = connection
85 else:
86 idString = ""
87 if self.__connection is not None:
88 self.__connection.close()
89
90 self.__connection = connection
91
92 connection.readyRead.connect(
93 lambda: self.__receiveJson(idString))
94 connection.disconnected.connect(
95 lambda: self.__handleDisconnect(idString))
96
97 @pyqtSlot()
98 def __handleDisconnect(self, idString):
99 """
100 Private slot handling a disconnect of the client.
101
102 @param idString id of the connection been disconnected
103 @type str
104 """
105 if idString:
106 if idString in self.__connections:
107 self.__connections[idString].close()
108 del self.__connections[idString]
109 else:
110 if self.__connection is not None:
111 self.__connection.close()
112
113 self.__connection = None
114
115 def connectionNames(self):
116 """
117 Public method to get the list of active connection names.
118
119 If this is not a multiplexing server, an empty list is returned.
120
121 @return list of active connection names
122 @rtype list of str
123 """
124 if self.__multiplex:
125 return list(self.__connections.keys())
126 else:
127 return []
128
129 @pyqtSlot()
130 def __receiveJson(self, idString):
131 """
132 Private slot handling received data from the client.
133
134 @param idString id of the connection been disconnected
135 @type str
136 """
137 if idString:
138 try:
139 connection = self.__connections[idString]
140 except KeyError:
141 connection = None
142 else:
143 connection = self.__connection
144
145 while connection and connection.canReadLine():
146 data = connection.readLine()
147 jsonLine = bytes(data).decode("utf-8", 'backslashreplace')
148
149 #- print("JSON Server ({0}): {1}".format(self.__name, jsonLine))
150 #- this is for debugging only
151
152 try:
153 clientDict = json.loads(jsonLine.strip())
154 except (TypeError, ValueError) as err:
155 E5MessageBox.critical(
156 None,
157 self.tr("JSON Protocol Error"),
158 self.tr("""<p>The response received from the client"""
159 """ could not be decoded. Please report"""
160 """ this issue with the received data to the"""
161 """ eric bugs email address.</p>"""
162 """<p>Error: {0}</p>"""
163 """<p>Data:<br/>{1}</p>""").format(
164 str(err), Utilities.html_encode(jsonLine.strip())),
165 E5MessageBox.StandardButtons(
166 E5MessageBox.Ok))
167 return
168
169 self.handleCall(clientDict["method"], clientDict["params"])
170
171 def sendJson(self, command, params, flush=False, idString=""):
172 """
173 Public method to send a single command to a client.
174
175 @param command command name to be sent
176 @type str
177 @param params dictionary of named parameters for the command
178 @type dict
179 @param flush flag indicating to flush the data to the socket
180 @type bool
181 @param idString id of the connection to send data to
182 @type str
183 """
184 commandDict = {
185 "jsonrpc": "2.0",
186 "method": command,
187 "params": params,
188 }
189 cmd = json.dumps(commandDict) + '\n'
190
191 if idString:
192 try:
193 connection = self.__connections[idString]
194 except KeyError:
195 connection = None
196 else:
197 connection = self.__connection
198
199 if connection is not None:
200 data = cmd.encode('utf8', 'backslashreplace')
201 length = "{0:09d}".format(len(data))
202 connection.write(length.encode() + data)
203 if flush:
204 connection.flush()
205
206 def startClient(self, interpreter, clientScript, clientArgs, idString="",
207 environment=None):
208 """
209 Public method to start a client process.
210
211 @param interpreter interpreter to be used for the client
212 @type str
213 @param clientScript path to the client script
214 @type str
215 @param clientArgs list of arguments for the client
216 @param idString id of the client to be started
217 @type str
218 @param environment dictionary of environment settings to pass
219 @type dict
220 @return flag indicating a successful client start and the exit code
221 in case of an issue
222 @rtype bool, int
223 """
224 if interpreter == "" or not Utilities.isinpath(interpreter):
225 return False
226
227 exitCode = None
228
229 proc = QProcess()
230 proc.setProcessChannelMode(
231 QProcess.ProcessChannelMode.ForwardedChannels)
232 if environment is not None:
233 env = QProcessEnvironment()
234 for key, value in list(environment.items()):
235 env.insert(key, value)
236 proc.setProcessEnvironment(env)
237 args = [clientScript, self.__hostAddress, str(self.serverPort())]
238 if idString:
239 args.append(idString)
240 args.extend(clientArgs)
241 proc.start(interpreter, args)
242 if not proc.waitForStarted(10000):
243 proc = None
244
245 if idString:
246 self.__clientProcesses[idString] = proc
247 if proc:
248 timer = QTimer()
249 timer.setSingleShot(True)
250 timer.start(30000) # 30s timeout
251 while (
252 idString not in self.connectionNames() and
253 timer.isActive()
254 ):
255 # Give the event loop the chance to process the new
256 # connection of the client (= slow start).
257 QCoreApplication.processEvents(
258 QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents)
259
260 # check if client exited prematurely
261 if proc.state() == QProcess.ProcessState.NotRunning:
262 exitCode = proc.exitCode()
263 proc = None
264 self.__clientProcesses[idString] = None
265 break
266 else:
267 if proc:
268 timer = QTimer()
269 timer.setSingleShot(True)
270 timer.start(1000) # 1s timeout
271 while timer.isActive():
272 # check if client exited prematurely
273 QCoreApplication.processEvents(
274 QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents)
275 if proc.state() == QProcess.ProcessState.NotRunning:
276 exitCode = proc.exitCode()
277 proc = None
278 break
279 self.__clientProcess = proc
280
281 return proc is not None, exitCode
282
283 def stopClient(self, idString=""):
284 """
285 Public method to stop a client process.
286
287 @param idString id of the client to be stopped
288 @type str
289 """
290 self.sendJson("Exit", {}, flush=True, idString=idString)
291
292 if idString:
293 try:
294 connection = self.__connections[idString]
295 except KeyError:
296 connection = None
297 else:
298 connection = self.__connection
299 if connection is not None:
300 connection.waitForDisconnected()
301
302 if idString:
303 with contextlib.suppress(KeyError):
304 if self .__clientProcesses[idString] is not None:
305 self .__clientProcesses[idString].close()
306 del self.__clientProcesses[idString]
307 else:
308 if self.__clientProcess is not None:
309 self.__clientProcess.close()
310 self.__clientProcess = None
311
312 def stopAllClients(self):
313 """
314 Public method to stop all clients.
315 """
316 clientNames = self.connectionNames()[:]
317 for clientName in clientNames:
318 self.stopClient(clientName)
319
320 #######################################################################
321 ## The following methods should be overridden by derived classes
322 #######################################################################
323
324 def handleCall(self, method, params):
325 """
326 Public method to handle a method call from the client.
327
328 Note: This is an empty implementation that must be overridden in
329 derived classes.
330
331 @param method requested method name
332 @type str
333 @param params dictionary with method specific parameters
334 @type dict
335 """
336 pass

eric ide

mercurial