src/eric7/RemoteServer/EricServer.py

branch
server
changeset 10555
08e853c0c77b
parent 10547
a357729cb749
child 10561
be23a662d709
equal deleted inserted replaced
10551:d80184d38152 10555:08e853c0c77b
7 Module implementing the eric remote server. 7 Module implementing the eric remote server.
8 """ 8 """
9 9
10 import io 10 import io
11 import json 11 import json
12 import select 12 import selectors
13 import socket 13 import socket
14 import struct 14 import struct
15 import sys 15 import sys
16 import traceback 16 import traceback
17 import types
17 import zlib 18 import zlib
18 19
19 from eric7.UI.Info import Version 20 from eric7.UI.Info import Version
20 # TODO: remove dependency on 'eric7.UI.Info' 21 # TODO: remove dependency on 'eric7.UI.Info'
21 22
22 from .EricRequestCategory import EricRequestCategory 23 from .EricRequestCategory import EricRequestCategory
24 from .EricServerDebuggerRequestHandler import EricServerDebuggerRequestHandler
25 from .EricServerFileSystemRequestHandler import EricServerFileSystemRequestHandler
23 26
24 27
25 class EricServer: 28 class EricServer:
26 """ 29 """
27 Class implementing the eric remote server. 30 Class implementing the eric remote server.
43 # handler(request:str, params:dict, reqestUuid:str) -> None 46 # handler(request:str, params:dict, reqestUuid:str) -> None
44 self.__registerInternalHandlers() 47 self.__registerInternalHandlers()
45 48
46 self.__connection = None 49 self.__connection = None
47 50
51 self.__selector = selectors.DefaultSelector()
52
53 # create and register the 'Debugger' request handler
54 self.__debuggerRequestHandler = EricServerDebuggerRequestHandler(self)
55 self.registerRequestHandler(
56 EricRequestCategory.Debugger,
57 self.__debuggerRequestHandler.handleRequest,
58 )
59
60 # create and register the 'File System' request handler
61 self.__fileSystemRequestHandler = EricServerFileSystemRequestHandler(self)
62 self.registerRequestHandler(
63 EricRequestCategory.FileSystem,
64 self.__fileSystemRequestHandler.handleRequest,
65 )
66
67 # TODO: 'Project' handler not implemented yet
68 # TODO: implement an 'EditorConfig' handler (?)
69
48 address = ("", port) 70 address = ("", port)
49 if socket.has_dualstack_ipv6() and useIPv6: 71 if socket.has_dualstack_ipv6() and useIPv6:
50 self.__socket = socket.create_server( 72 self.__socket = socket.create_server(
51 address, family=socket.AF_INET6, dualstack_ipv6=True 73 address, family=socket.AF_INET6, dualstack_ipv6=True
52 ) 74 )
53 else: 75 else:
54 self.__socket = socket.create_server( 76 self.__socket = socket.create_server(address, family=socket.AF_INET)
55 address, family=socket.AF_INET 77
56 ) 78 def getSelector(self):
79 """
80 Public method to get a reference to the selector object.
81 """
82 return self.__selector
57 83
58 ####################################################################### 84 #######################################################################
59 ## Methods for receiving requests and sending the results. 85 ## Methods for receiving requests and sending the results.
60 ####################################################################### 86 #######################################################################
61 87
71 @type dict 97 @type dict
72 @param reqestUuid UUID of the associated request as sent by the eric IDE 98 @param reqestUuid UUID of the associated request as sent by the eric IDE
73 (defaults to "", i.e. no UUID received) 99 (defaults to "", i.e. no UUID received)
74 @type str 100 @type str
75 """ 101 """
76 commandDict = { 102 if self.__connection is not None:
77 "jsonrpc": "2.0", 103 commandDict = {
78 "category": category, 104 "jsonrpc": "2.0",
79 "reply": reply, 105 "category": category,
80 "params": params, 106 "reply": reply,
81 "uuid": reqestUuid, 107 "params": params,
82 } 108 "uuid": reqestUuid,
83 data = json.dumps(commandDict).encode("utf8", "backslashreplace") 109 }
110 self.sendJsonCommand(commandDict, self.__connection)
111
112 def sendJsonCommand(self, jsonCommand, sock):
113 """
114 Public method to send a JSON encoded command/response via a given socket.
115
116 @param jsonCommand dictionary containing the command data or a JSON encoded
117 command string
118 @type dict or str
119 @param sock reference to the socket to send the data to
120 @type socket.socket
121 """
122 if isinstance(jsonCommand, dict):
123 jsonCommand = json.dumps(jsonCommand)
124 data = jsonCommand.encode("utf8", "backslashreplace")
84 header = struct.pack(b"!II", len(data), zlib.adler32(data) & 0xFFFFFFFF) 125 header = struct.pack(b"!II", len(data), zlib.adler32(data) & 0xFFFFFFFF)
85 self.__connection.sendall(header) 126 sock.sendall(header)
86 self.__connection.sendall(data) 127 sock.sendall(data)
87 128
88 def __receiveBytes(self, length): 129 def __receiveBytes(self, length, sock):
89 """ 130 """
90 Private method to receive the given length of bytes. 131 Private method to receive the given length of bytes.
91 132
92 @param length bytes to receive 133 @param length bytes to receive
93 @type int 134 @type int
135 @param sock reference to the socket to receive the data from
136 @type socket.socket
94 @return received bytes or None if connection closed 137 @return received bytes or None if connection closed
95 @rtype bytes 138 @rtype bytes
96 """ 139 """
97 data = bytearray() 140 data = bytearray()
98 while len(data) < length: 141 while len(data) < length:
99 newData = self.__connection.recv(length - len(data)) 142 try:
100 if not newData: 143 newData = sock.recv(length - len(data))
101 return None 144 if not newData:
102 145 print(str(newData))
103 data += newData 146 return None
147
148 data += newData
149 except OSError as err:
150 if err.errno != 11:
151 data = None # in case some data was received already
152 break
104 return data 153 return data
105 154
106 def __receiveJson(self): 155 def receiveJsonCommand(self, sock):
107 """ 156 """
108 Private method to receive a JSON encoded command and data from the 157 Public method to receive a JSON encoded command and data.
109 server. 158
110 159 @param sock reference to the socket to receive the data from
111 @return tuple containing the received service category, the command, 160 @type socket.socket
112 a dictionary containing the associated data and the UUID of the 161 @return dictionary containing the JSON command data or None to signal
113 request 162 an issue while receiving data
114 @rtype tuple of (int, str, dict, str) 163 @rtype dict
115 """ 164 """
116 # step 1: receive the data 165 if self.isSocketClosed(sock):
117 header = self.__receiveBytes(struct.calcsize(b"!II")) 166 return None
167
168 header = self.__receiveBytes(struct.calcsize(b"!II"), sock)
118 if not header: 169 if not header:
119 return EricRequestCategory.Error, None, None, None 170 return {}
120 171
121 length, datahash = struct.unpack(b"!II", header) 172 length, datahash = struct.unpack(b"!II", header)
122 173
123 length = int(length) 174 length = int(length)
124 data = self.__receiveBytes(length) 175 data = self.__receiveBytes(length, sock)
176 if data is None:
177 return None
178
125 if not data or zlib.adler32(data) & 0xFFFFFFFF != datahash: 179 if not data or zlib.adler32(data) & 0xFFFFFFFF != datahash:
126 self.sendJson( 180 self.sendJson(
127 category=EricRequestCategory.Error, 181 category=EricRequestCategory.Error,
128 reply="ClientChecksumException", 182 reply="EricServerChecksumException",
129 params={ 183 params={
130 "ExceptionType": "ProtocolChecksumError", 184 "ExceptionType": "ProtocolChecksumError",
131 "ExceptionValue": "The checksum of the data does not match.", 185 "ExceptionValue": "The checksum of the data does not match.",
132 "ProtocolData": data.decode("utf8", "backslashreplace"), 186 "ProtocolData": data.decode("utf8", "backslashreplace"),
133 }, 187 },
134 ) 188 )
135 return EricRequestCategory.Error, None, None, None 189 return {}
136 190
137 # step 2: decode and convert the data 191 jsonStr = data.decode("utf8", "backslashreplace")
138 jsonString = data.decode("utf8", "backslashreplace")
139 try: 192 try:
140 requestDict = json.loads(jsonString.strip()) 193 return json.loads(jsonStr.strip())
141 except (TypeError, ValueError) as err: 194 except (TypeError, ValueError) as err:
142 self.sendJson( 195 self.sendJson(
143 category=EricRequestCategory.Error, 196 category=EricRequestCategory.Error,
144 reply="ClientException", 197 reply="EricServerException",
145 params={ 198 params={
146 "ExceptionType": "ProtocolError", 199 "ExceptionType": "ProtocolError",
147 "ExceptionValue": str(err), 200 "ExceptionValue": str(err),
148 "ProtocolData": jsonString.strip(), 201 "ProtocolData": jsonStr.strip(),
149 }, 202 },
150 ) 203 )
204 return {}
205
206 def __receiveJson(self):
207 """
208 Private method to receive a JSON encoded command and data from the
209 server.
210
211 @return tuple containing the received service category, the command,
212 a dictionary containing the associated data and the UUID of the
213 request
214 @rtype tuple of (int, str, dict, str)
215 """
216 requestDict = self.receiveJsonCommand(self.__connection)
217
218 if not requestDict:
151 return EricRequestCategory.Error, None, None, None 219 return EricRequestCategory.Error, None, None, None
152 220
153 category = requestDict["category"] 221 category = requestDict["category"]
154 request = requestDict["request"] 222 request = requestDict["request"]
155 params = requestDict["params"] 223 params = requestDict["params"]
156 reqestUuid = requestDict["uuid"] 224 reqestUuid = requestDict["uuid"]
157 225
158 return category, request, params, reqestUuid 226 return category, request, params, reqestUuid
159 227
228 def isSocketClosed(self, sock):
229 """
230 Public method to check, if a given socket is closed.
231
232 @param sock reference to the socket to be checked
233 @type socket.socket
234 @return flag indicating a closed state
235 @rtype bool
236 """
237 try:
238 # this will try to read bytes without removing them from buffer (peek only)
239 data = sock.recv(16, socket.MSG_PEEK)
240 if len(data) == 0:
241 return True
242 except BlockingIOError:
243 return False # socket is open and reading from it would block
244 except ConnectionError:
245 return True # socket was closed for some other reason
246 except Exception:
247 return False
248 return False
249
160 ####################################################################### 250 #######################################################################
161 ## Methods for the server main loop. 251 ## Methods for the server main loop.
162 ####################################################################### 252 #######################################################################
163 253
164 def __shutdown(self): 254 def __shutdown(self):
165 """ 255 """
166 Private method to shut down the server. 256 Private method to shut down the server.
167 """ 257 """
258 self.__closeIdeConnection()
259
260 print("Stop listening for 'eric-ide' connections.")
168 self.__socket.shutdown(socket.SHUT_RDWR) 261 self.__socket.shutdown(socket.SHUT_RDWR)
169 self.__socket.close() 262 self.__socket.close()
263
264 self.__selector.close()
265
266 def __acceptIdeConnection(self, sock):
267 """
268 Private method to accept the connection on the listening IDE server socket.
269
270 @param sock reference to the listening socket
271 @type socket.socket
272 """
273 self.__connection, address = sock.accept() # Should be ready to read
274 print(f"'eric-ide' connection from {address[0]}, port {address[1]}")
275 self.__connection.setblocking(False)
276 data = types.SimpleNamespace(
277 name="eric-ide", address=address, handler=self.__serviceIdeConnection
278 )
279 events = selectors.EVENT_READ
280 self.__selector.register(self.__connection, events, data=data)
281
282 def __closeIdeConnection(self):
283 """
284 Private method to close the connection to an eric-ide.
285 """
286 if self.__connection is not None:
287 print(
288 f"Closing 'eric-ide' connection to {self.__connection.getpeername()}."
289 )
290 self.__selector.unregister(self.__connection)
291 self.__connection.shutdown(socket.SHUT_RDWR)
292 self.__connection.close()
293 self.__connection = None
294
295 self.__debuggerRequestHandler.shutdownClients()
296
297 def __serviceIdeConnection(self, key):
298 """
299 Private method to service the eric-ide connection.
300
301 @param key reference to the SelectorKey object associated with the connection
302 to be serviced
303 @type selectors.SelectorKey
304 """
305 if key.data.name == "eric-ide":
306 category, request, params, reqestUuid = self.__receiveJson()
307 if category == EricRequestCategory.Error or request is None:
308 self.__closeIdeConnection()
309 return
310
311 if category == EricRequestCategory.Server:
312 if request.lower() == "shutdown":
313 self.__shouldStop = True
314 return
315
316 self.__handleRequest(category, request, params, reqestUuid)
170 317
171 def run(self): 318 def run(self):
172 """ 319 """
173 Public method implementing the remote server main loop. 320 Public method implementing the remote server main loop.
174 321
180 </ul> 327 </ul>
181 328
182 @return flag indicating a clean shutdown 329 @return flag indicating a clean shutdown
183 @rtype bool 330 @rtype bool
184 """ 331 """
185 shutdown = False
186 cleanExit = True 332 cleanExit = True
333 self.__shouldStop = False
187 334
188 # listen on the server socket for new connections 335 # listen on the server socket for new connections
189 self.__socket.listen(1) 336 self.__socket.listen(1)
337 self.__socket.setblocking(False)
338 print(f"Listening for 'eric-ide' connections on {self.__socket.getsockname()}")
339 data = types.SimpleNamespace(
340 name="server", acceptHandler=self.__acceptIdeConnection
341 )
342 self.__selector.register(self.__socket, selectors.EVENT_READ, data=data)
343
344 self.__debuggerRequestHandler.initServerSocket()
190 345
191 while True: 346 while True:
192 try: 347 try:
193 # accept the next pending connection 348 events = self.__selector.select(timeout=None)
194 print("Waiting for connection...") 349 for key, mask in events:
195 self.__connection, address = self.__socket.accept() 350 if key.data.name == "server":
196 print(f"Connection from {address[0]}, port {address[1]}") 351 # it is an event for a server socket
197 352 key.data.acceptHandler(key.fileobj)
198 selectErrors = 0 353 else:
199 while selectErrors <= 10: # selected arbitrarily 354 key.data.handler(key)
200 try:
201 rrdy, wrdy, xrdy = select.select([self.__connection], [], [])
202
203 # Just waiting for self.__connection. Therefore no check
204 # needed.
205 category, request, params, reqestUuid = self.__receiveJson()
206 if category == EricRequestCategory.Error or request is None:
207 selectErrors += 1
208 elif category == EricRequestCategory.Server:
209 if request.lower() == "exit":
210 break
211 elif request.lower() == "shutdown":
212 shutdown = True
213 break
214 else:
215 self.__handleRequest(
216 category, request, params, reqestUuid
217 )
218 else:
219 self.__handleRequest(category, request, params, reqestUuid)
220
221 # reset select errors
222 selectErrors = 0
223
224 except (select.error, socket.error):
225 selectErrors += 1
226 355
227 except KeyboardInterrupt: 356 except KeyboardInterrupt:
228 # intercept user pressing Ctrl+C 357 # intercept user pressing Ctrl+C
229 shutdown = True 358 self.__shouldStop = True
230 359
231 except Exception: 360 except Exception:
232 exctype, excval, exctb = sys.exc_info() 361 exctype, excval, exctb = sys.exc_info()
233 tbinfofile = io.StringIO() 362 tbinfofile = io.StringIO()
234 traceback.print_tb(exctb, None, tbinfofile) 363 traceback.print_tb(exctb, None, tbinfofile)
235 tbinfofile.seek(0) 364 tbinfofile.seek(0)
236 tbinfo = tbinfofile.read() 365 tbinfo = tbinfofile.read()
237 366
238 print(f"{str(exctype)} / {str(excval)} / {tbinfo}") 367 print(f"{str(exctype)} / {str(excval)} / {tbinfo}")
239 368
240 shutdown = True 369 self.__shouldStop = True
241 cleanExit = False 370 cleanExit = False
242 371
243 if self.__connection is not None: 372 if self.__shouldStop:
244 self.__connection.shutdown(socket.SHUT_RDWR)
245 self.__connection.close()
246 self.__connection = None
247
248 if shutdown:
249 # exit the outer loop and shut down the server 373 # exit the outer loop and shut down the server
250 self.__shutdown() 374 self.__shutdown()
251 break 375 break
252 376
253 return cleanExit 377 return cleanExit
322 @exception ValueError raised to indicate an invalid or unsupported request 446 @exception ValueError raised to indicate an invalid or unsupported request
323 handler catehory 447 handler catehory
324 """ 448 """
325 try: 449 try:
326 handler = self.__requestCategoryHandlerRegistry[category] 450 handler = self.__requestCategoryHandlerRegistry[category]
451 handler(request=request, params=params, reqestUuid=reqestUuid)
327 except KeyError: 452 except KeyError:
328 if category < EricRequestCategory.UserCategory:
329 # it is an internally supported category
330 if category == EricRequestCategory.FileSystem:
331 from .EricServerFileSystemRequestHandler import (
332 EricServerFileSystemRequestHandler,
333 )
334 handler = EricServerFileSystemRequestHandler(self).handleRequest
335
336 elif category == EricRequestCategory.Project:
337 # TODO: 'Project' handler not implemented yet
338 handler = None
339
340 elif category == EricRequestCategory.Debugger:
341 # TODO: 'Debugger' handler not implemented yet
342 handler = None
343
344 # TODO: implement an 'EditorConfig' handler (?)
345
346 else:
347 # That internal category does not exist.
348 handler = None
349
350 self.registerRequestHandler(category, handler)
351 else:
352 handler = None
353
354 if handler is not None:
355 handler(request=request, params=params, reqestUuid=reqestUuid)
356 else:
357 self.sendJson( 453 self.sendJson(
358 category=EricRequestCategory.Error, 454 category=EricRequestCategory.Error,
359 reply="UnsupportedServiceCategory", 455 reply="UnsupportedServiceCategory",
360 params={"Category": category}, 456 params={"Category": category},
361 ) 457 )

eric ide

mercurial