|
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 ) |