src/eric7/EricNetwork/EricJsonServer.py

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

eric ide

mercurial