src/eric7/RemoteServer/EricServer.py

branch
server
changeset 10531
3308e8349e4c
child 10539
4274f189ff78
equal deleted inserted replaced
10530:684f491a3bfc 10531:3308e8349e4c
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the eric remote server.
8 """
9
10 import io
11 import json
12 import select
13 import socket
14 import struct
15 import sys
16 import traceback
17 import zlib
18
19 from eric7.UI.Info import Version
20
21 from .EricRequestCategory import EricRequestCategory
22
23
24 class EricServer:
25 """
26 Class implementing the eric remote server.
27 """
28
29 def __init__(self, port=42024, useIPv6=False):
30 """
31 Constructor
32
33 @param port port to listen on (defaults to 42024)
34 @type int (optional)
35 @param useIPv6 flag indicating to use IPv6 protocol (defaults to False)
36 @type bool (optional)
37 """
38 self.__requestCategoryHandlerRegistry = {
39 # Dictionary containing the defined and registered request category
40 # handlers. The key is the request category and the value is the respective
41 # handler method. This method must have the signature:
42 # handler(request:str, params:dict, reqestUuid:str) -> None
43 EricRequestCategory.Debugger: None, # TODO: not implemented yet
44 EricRequestCategory.Echo: self.__handleEchoRequest,
45 EricRequestCategory.FileSystem: None, # TODO: not implemented yet
46 EricRequestCategory.Project: None, # TODO: not implemented yet
47 EricRequestCategory.Server: self.__handleServerRequest
48 }
49
50 self.__connection = None
51
52 address = ("", port)
53 if socket.has_dualstack_ipv6() and useIPv6:
54 self.__socket = socket.create_server(
55 address, family=socket.AF_INET6, dualstack_ipv6=True
56 )
57 else:
58 self.__socket = socket.create_server(
59 address, family=socket.AF_INET
60 )
61
62 #######################################################################
63 ## Methods for receiving requests and sending the results.
64 #######################################################################
65
66 def sendJson(self, category, reply, params, reqestUuid=""):
67 """
68 Public method to send a single refactoring command to the server.
69
70 @param category service category
71 @type EricRequestCategory
72 @param reply reply name to be sent
73 @type str
74 @param params dictionary of named parameters for the request
75 @type dict
76 @param reqestUuid UUID of the associated request as sent by the eric IDE
77 (defaults to "", i.e. no UUID received)
78 @type str
79 """
80 commandDict = {
81 "jsonrpc": "2.0",
82 "category": category,
83 "reply": reply,
84 "params": params,
85 "uuid": reqestUuid,
86 }
87 data = json.dumps(commandDict).encode("utf8", "backslashreplace")
88 header = struct.pack(b"!II", len(data), zlib.adler32(data) & 0xFFFFFFFF)
89 self.__connection.sendall(header)
90 self.__connection.sendall(data)
91
92 def __receiveBytes(self, length):
93 """
94 Private method to receive the given length of bytes.
95
96 @param length bytes to receive
97 @type int
98 @return received bytes or None if connection closed
99 @rtype bytes
100 """
101 data = bytearray()
102 while len(data) < length:
103 newData = self.__connection.recv(length - len(data))
104 if not newData:
105 return None
106
107 data += newData
108 return data
109
110 def __receiveJson(self):
111 """
112 Private method to receive a JSON encoded command and data from the
113 server.
114
115 @return tuple containing the received service category, the command,
116 a dictionary containing the associated data and the UUID of the
117 request
118 @rtype tuple of (int, str, dict, str)
119 """
120 # step 1: receive the data
121 header = self.__receiveBytes(struct.calcsize(b"!II"))
122 if not header:
123 return EricRequestCategory.Error, None, None, None
124
125 length, datahash = struct.unpack(b"!II", header)
126
127 length = int(length)
128 data = self.__receiveBytes(length)
129 if not data or zlib.adler32(data) & 0xFFFFFFFF != datahash:
130 self.sendJson(
131 category=EricRequestCategory.Error,
132 reply="ClientChecksumException",
133 params={
134 "ExceptionType": "ProtocolChecksumError",
135 "ExceptionValue": "The checksum of the data does not match.",
136 "ProtocolData": data.decode("utf8", "backslashreplace"),
137 },
138 )
139 return EricRequestCategory.Error, None, None, None
140
141 # step 2: decode and convert the data
142 jsonString = data.decode("utf8", "backslashreplace")
143 try:
144 requestDict = json.loads(jsonString.strip())
145 except (TypeError, ValueError) as err:
146 self.sendJson(
147 category=EricRequestCategory.Error,
148 reply="ClientException",
149 params={
150 "ExceptionType": "ProtocolError",
151 "ExceptionValue": str(err),
152 "ProtocolData": jsonString.strip(),
153 },
154 )
155 return EricRequestCategory.Error, None, None, None
156
157 category = requestDict["category"]
158 request = requestDict["request"]
159 params = requestDict["params"]
160 reqestUuid = requestDict["uuid"]
161
162 return category, request, params, reqestUuid
163
164 #######################################################################
165 ## Methods for the server main loop.
166 #######################################################################
167
168 def __shutdown(self):
169 """
170 Private method to shut down the server.
171 """
172 self.__socket.shutdown(socket.SHUT_RDWR)
173 self.__socket.close()
174
175 def run(self):
176 """
177 Public method implementing the remote server main loop.
178
179 Exiting the inner loop, that receives and dispatches the requests, will
180 cause the server to stop and exit. The main loop handles these requests.
181 <ul>
182 <li>exit - exit the handler loop and wait for the next connection</li>
183 <li>shutdown - exit the handler loop and perform a clean shutdown</li>
184 </ul>
185
186 @return flag indicating a clean shutdown
187 @rtype bool
188 """
189 shutdown = False
190 cleanExit = True
191
192 # listen on the server socket for new connections
193 self.__socket.listen(1)
194
195 while True:
196 try:
197 # accept the next pending connection
198 print("Waiting for connection...")
199 self.__connection, address = self.__socket.accept()
200 print(f"Connection from {address[0]}, port {address[1]}")
201
202 selectErrors = 0
203 while selectErrors <= 10: # selected arbitrarily
204 try:
205 rrdy, wrdy, xrdy = select.select([self.__connection], [], [])
206
207 # Just waiting for self.__connection. Therefore no check
208 # needed.
209 category, request, params, reqestUuid = self.__receiveJson()
210 if category == EricRequestCategory.Error or request is None:
211 selectErrors += 1
212 elif category == EricRequestCategory.Server:
213 if request.lower() == "exit":
214 break
215 elif request.lower() == "shutdown":
216 shutdown = True
217 break
218 else:
219 self.__handleRequest(
220 category, request, params, reqestUuid
221 )
222 else:
223 self.__handleRequest(category, request, params, reqestUuid)
224
225 # reset select errors
226 selectErrors = 0
227
228 except (select.error, socket.error):
229 selectErrors += 1
230
231 except KeyboardInterrupt:
232 # intercept user pressing Ctrl+C
233 shutdown = True
234
235 except Exception:
236 exctype, excval, exctb = sys.exc_info()
237 tbinfofile = io.StringIO()
238 traceback.print_tb(exctb, None, tbinfofile)
239 tbinfofile.seek(0)
240 tbinfo = tbinfofile.read()
241
242 print(f"{str(exctype)} / {str(excval)} / {tbinfo}")
243
244 shutdown = True
245 cleanExit = False
246
247 if self.__connection is not None:
248 self.__connection.shutdown(socket.SHUT_RDWR)
249 self.__connection.close()
250 self.__connection = None
251
252 if shutdown:
253 # exit the outer loop and shut down the server
254 self.__shutdown()
255 break
256
257 return cleanExit
258
259 #######################################################################
260 ## Request handler methods.
261 #######################################################################
262
263 def __handleRequest(self, category, request, params, reqestUuid):
264 """
265 Private method handling or dispatching the received requests.
266
267 @param category category of the request
268 @type EricRequestCategory
269 @param request request name
270 @type str
271 @param params request parameters
272 @type dict
273 @param reqestUuid UUID of the associated request as sent by the eric IDE
274 (defaults to "", i.e. no UUID received)
275 @type str
276 """
277 try:
278 handler = self.__requestCategoryHandlerRegistry[category]
279 if handler is None:
280 raise ValueError("invalid handler function")
281 handler(request=request, params=params, reqestUuid=reqestUuid)
282 except (KeyError, ValueError):
283 self.sendJson(
284 category=EricRequestCategory.Error,
285 reply="UnsupportedServiceCategory",
286 params={"Category": category},
287 )
288
289 def __handleEchoRequest(self, request, params, reqestUuid):
290 """
291 Private method to handle an 'Echo' request.
292
293 @param request request name
294 @type str
295 @param params request parameters
296 @type dict
297 @param reqestUuid UUID of the associated request as sent by the eric IDE
298 (defaults to "", i.e. no UUID received)
299 @type str
300 """
301 self.sendJson(
302 category=EricRequestCategory.Echo,
303 reply="Echo",
304 params=params,
305 reqestUuid=reqestUuid,
306 )
307
308 def __handleServerRequest(self, request, params, reqestUuid):
309 """
310 Private method to handle a 'Server' request.
311
312 @param request request name
313 @type str
314 @param params request parameters
315 @type dict
316 @param reqestUuid UUID of the associated request as sent by the eric IDE
317 (defaults to "", i.e. no UUID received)
318 @type str
319 """
320 # 'Exit' and 'Shutdown' are handled in the 'run()' method.
321
322 if request.lower() == "versions":
323 self.sendJson(
324 category=EricRequestCategory.Server,
325 reply="Versions",
326 params={
327 "python": sys.version.split()[0],
328 "py_bitsize": "64-Bit" if sys.maxsize > 2**32 else "32-Bit",
329 "version": Version,
330 },
331 reqestUuid=reqestUuid,
332 )

eric ide

mercurial