src/eric7/EricNetwork/EricJsonServer.py

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

eric ide

mercurial