|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing the debugger request handler of the eric-ide server. |
|
8 """ |
|
9 |
|
10 import json |
|
11 import os |
|
12 import selectors |
|
13 import socket |
|
14 import subprocess |
|
15 import sys |
|
16 import types |
|
17 |
|
18 from .EricRequestCategory import EricRequestCategory |
|
19 |
|
20 class EricServerDebuggerRequestHandler: |
|
21 """ |
|
22 Class implementing the debugger request handler of the eric-ide server. |
|
23 """ |
|
24 |
|
25 def __init__(self, server): |
|
26 """ |
|
27 Constructor |
|
28 |
|
29 @param server reference to the eric-ide server object |
|
30 @type EricServer |
|
31 """ |
|
32 self.__server = server |
|
33 |
|
34 self.__requestMethodMapping = { |
|
35 "StartClient": self.__startClient, |
|
36 "StopClient": self.__stopClient, |
|
37 "DebugClientCommand": self.__relayDebugClientCommand |
|
38 } |
|
39 |
|
40 self.__mainClientId = None |
|
41 self.__client = None |
|
42 self.__pendingConnections = [] |
|
43 self.__connections = {} |
|
44 |
|
45 address = ("127.0.0.1", 0) |
|
46 self.__socket = socket.create_server(address, family=socket.AF_INET) |
|
47 |
|
48 def initServerSocket(self): |
|
49 """ |
|
50 Public method to initialize the server socket listening for debug client |
|
51 connections. |
|
52 """ |
|
53 # listen on the debug server socket |
|
54 self.__socket.listen() |
|
55 self.__socket.setblocking(False) |
|
56 print( |
|
57 f"Listening for 'debug client' connections on" |
|
58 f" {self.__socket.getsockname()}" |
|
59 ) |
|
60 data = types.SimpleNamespace( |
|
61 name="server", acceptHandler=self.__acceptDbgClientConnection |
|
62 ) |
|
63 self.__server.getSelector().register( |
|
64 self.__socket, selectors.EVENT_READ, data=data |
|
65 ) |
|
66 |
|
67 def handleRequest(self, request, params, reqestUuid): |
|
68 """ |
|
69 Public method handling the received debugger requests. |
|
70 |
|
71 @param request request name |
|
72 @type str |
|
73 @param params dictionary containing the request parameters |
|
74 @type dict |
|
75 @param reqestUuid UUID of the associated request as sent by the eric IDE |
|
76 @type str |
|
77 """ |
|
78 try: |
|
79 result = self.__requestMethodMapping[request](params) |
|
80 if result: |
|
81 self.__server.sendJson( |
|
82 category=EricRequestCategory.Debugger, |
|
83 reply=request, |
|
84 params=result, |
|
85 reqestUuid=reqestUuid, |
|
86 ) |
|
87 |
|
88 except KeyError: |
|
89 self.__server.sendJson( |
|
90 category=EricRequestCategory.Debugger, |
|
91 reply="DebuggerRequestError", |
|
92 params={"Error": f"Request type '{request}' is not supported."}, |
|
93 ) |
|
94 |
|
95 ####################################################################### |
|
96 ## DebugServer like methods. |
|
97 ####################################################################### |
|
98 |
|
99 def __acceptDbgClientConnection(self, sock): |
|
100 """ |
|
101 Private method to accept the connection on the listening debug server socket. |
|
102 |
|
103 @param sock reference to the listening socket |
|
104 @type socket.socket |
|
105 """ |
|
106 connection, address = sock.accept() # Should be ready to read |
|
107 print(f"'Debug client' connection from {address[0]}, port {address[1]}") |
|
108 connection.setblocking(False) |
|
109 self.__pendingConnections.append(connection) |
|
110 |
|
111 data = types.SimpleNamespace( |
|
112 name="debug_client", |
|
113 address=address, |
|
114 handler=self.__serviceDbgClientConnection, |
|
115 ) |
|
116 events = selectors.EVENT_READ |
|
117 self.__server.getSelector().register(connection, events, data=data) |
|
118 |
|
119 def __serviceDbgClientConnection(self, key): |
|
120 """ |
|
121 Private method to service the debug client connection. |
|
122 |
|
123 @param key reference to the SelectorKey object associated with the connection |
|
124 to be serviced |
|
125 @type selectors.SelectorKey |
|
126 """ |
|
127 sock = key.fileobj |
|
128 data = self.__server.receiveJsonCommand(sock) |
|
129 |
|
130 if data is None: |
|
131 # socket was closed by debug client |
|
132 self.__clientSocketDisconnected(sock) |
|
133 elif data: |
|
134 method = data["method"] |
|
135 if method == "DebuggerId" and sock in self.__pendingConnections: |
|
136 debuggerId = data['params']['debuggerId'] |
|
137 self.__connections[debuggerId] = sock |
|
138 self.__pendingConnections.remove(sock) |
|
139 if self.__mainClientId is None: |
|
140 self.__mainClientId = debuggerId |
|
141 |
|
142 elif method == "ResponseBanner": |
|
143 # add an indicator for the eric-ide server |
|
144 data["params"]["platform"] += " (eric-ide Server)" |
|
145 |
|
146 # pass on the data to the eric-ide |
|
147 jsonStr = json.dumps(data) |
|
148 print("Client Response:", jsonStr) |
|
149 self.__server.sendJson( |
|
150 category=EricRequestCategory.Debugger, |
|
151 reply="DebugClientResponse", |
|
152 params={"response": jsonStr}, |
|
153 ) |
|
154 |
|
155 def __clientSocketDisconnected(self, sock): |
|
156 """ |
|
157 Private slot handling a socket disconnecting. |
|
158 |
|
159 @param sock reference to the disconnected socket |
|
160 @type QTcpSocket |
|
161 """ |
|
162 self.__server.getSelector().unregister(sock) |
|
163 |
|
164 for debuggerId in list(self.__connections): |
|
165 if self.__connections[debuggerId] is sock: |
|
166 del self.__connections[debuggerId] |
|
167 self.__server.sendJson( |
|
168 category=EricRequestCategory.Debugger, |
|
169 reply="DebugClientDisconnected", |
|
170 params={"debugger_id": debuggerId}, |
|
171 ) |
|
172 |
|
173 if debuggerId == self.__mainClientId: |
|
174 self.__mainClientId = None |
|
175 |
|
176 break |
|
177 else: |
|
178 if sock in self.__pendingConnections: |
|
179 self.__pendingConnections.remove(sock) |
|
180 |
|
181 sock.shutdown(socket.SHUT_RDWR) |
|
182 sock.close() |
|
183 |
|
184 if not self.__connections: |
|
185 # no active connections anymore |
|
186 self.__server.sendJson( |
|
187 category=EricRequestCategory.Debugger, |
|
188 reply="LastDebugClientExited", |
|
189 params={}, |
|
190 ) |
|
191 |
|
192 def __serviceDbgClientStdoutStderr(self, key): |
|
193 """ |
|
194 Private method to service the debug client stdout and stderr channels. |
|
195 |
|
196 @param key reference to the SelectorKey object associated with the connection |
|
197 to be serviced |
|
198 @type selectors.SelectorKey |
|
199 """ |
|
200 data = key.fileobj.read() |
|
201 if key.data.name == "debug_client_stdout": |
|
202 # TODO: stdout handling not implemented yet |
|
203 print("stdout:", data) |
|
204 elif key.data.name == "debug_client_stderr": |
|
205 # TODO: stderr handling not implemented yet |
|
206 print("stderr:", data) |
|
207 |
|
208 def shutdownClients(self): |
|
209 """ |
|
210 Public method to shut down all connected clients. |
|
211 """ |
|
212 if not self.__client: |
|
213 # no client started yet |
|
214 return |
|
215 |
|
216 while self.__pendingConnections: |
|
217 sock = self.__pendingConnections.pop() |
|
218 commandDict = self.__prepareClientCommand("RequestShutdown", {}) |
|
219 self.__server.sendJsonCommand(commandDict, sock) |
|
220 self.__shutdownSocket("", sock) |
|
221 |
|
222 while self.__connections: |
|
223 debuggerId, sock = self.__connections.popitem() |
|
224 commandDict = self.__prepareClientCommand("RequestShutdown", {}) |
|
225 self.__server.sendJsonCommand(commandDict, sock) |
|
226 self.__shutdownSocket(debuggerId, sock) |
|
227 |
|
228 # reinitialize |
|
229 self.__mainClientId = None |
|
230 self.__client = None |
|
231 |
|
232 # no active connections anymore |
|
233 self.__server.sendJson( |
|
234 category=EricRequestCategory.Debugger, |
|
235 reply="LastDebugClientExited", |
|
236 params={}, |
|
237 ) |
|
238 |
|
239 def __shutdownSocket(self, debuggerId, sock): |
|
240 """ |
|
241 Private slot to shut down a socket. |
|
242 |
|
243 @param debuggerId ID of the debugger the socket belongs to |
|
244 @type str |
|
245 @param sock reference to the socket |
|
246 @type socket.socket |
|
247 """ |
|
248 self.__server.getSelector().unregister(sock) |
|
249 sock.shutdown(socket.SHUT_RDWR) |
|
250 sock.close() |
|
251 |
|
252 if debuggerId: |
|
253 self.__server.sendJson( |
|
254 category=EricRequestCategory.Debugger, |
|
255 reply="DebugClientDisconnected", |
|
256 params={"debugger_id": debuggerId}, |
|
257 ) |
|
258 |
|
259 def __prepareClientCommand(self, command, params): |
|
260 """ |
|
261 Private method to prepare a command dictionary for the debug client. |
|
262 |
|
263 @param command command to be sent |
|
264 @type str |
|
265 @param params dictionary containing the command parameters |
|
266 @type dict |
|
267 @return completed command dictionary to be sent to the debug client |
|
268 @rtype dict |
|
269 """ |
|
270 return { |
|
271 "jsonrpc": "2.0", |
|
272 "method": command, |
|
273 "params": params, |
|
274 } |
|
275 |
|
276 ####################################################################### |
|
277 ## Individual request handler methods. |
|
278 ####################################################################### |
|
279 |
|
280 def __startClient(self, params): |
|
281 """ |
|
282 Private method to start a debug client process. |
|
283 |
|
284 @param params dictionary containing the request data |
|
285 @type dict |
|
286 """ |
|
287 # 1. stop an already started debug client |
|
288 if self.__client is not None: |
|
289 self.__client.terminate() |
|
290 self.__client = None |
|
291 |
|
292 # 2. start a debug client |
|
293 debugClient = os.path.abspath( |
|
294 os.path.join( |
|
295 os.path.dirname(__file__), |
|
296 "..", |
|
297 "DebugClients", |
|
298 "Python", |
|
299 "DebugClient.py", |
|
300 ) |
|
301 ) |
|
302 ipaddr, port = self.__socket.getsockname() |
|
303 args = [sys.executable, debugClient] |
|
304 args.extend(params["arguments"]) |
|
305 args.extend([str(port), "True", ipaddr]) |
|
306 |
|
307 self.__client = subprocess.Popen( |
|
308 args, stdout=subprocess.PIPE, stderr=subprocess.PIPE |
|
309 ) |
|
310 # TODO: register stdin & stderr with selector |
|
311 |
|
312 def __stopClient(self, params): |
|
313 """ |
|
314 Private method to stop the current debug client process. |
|
315 |
|
316 @param params dictionary containing the request data |
|
317 @type dict |
|
318 @return dictionary containing the reply data |
|
319 @rtype dict |
|
320 """ |
|
321 self.shutdownClients() |
|
322 |
|
323 return {"ok": True} |
|
324 |
|
325 def __relayDebugClientCommand(self, params): |
|
326 """ |
|
327 Private method to relay a debug client command to the client. |
|
328 |
|
329 @param params dictionary containing the request data |
|
330 @type dict |
|
331 """ |
|
332 debuggerId = params["debugger_id"] |
|
333 jsonStr = params["command"] |
|
334 print(debuggerId, "->", jsonStr) |
|
335 |
|
336 if not debuggerId and self.__mainClientId: |
|
337 debuggerId = self.__mainClientId |
|
338 |
|
339 try: |
|
340 sock = self.__connections[debuggerId] |
|
341 except KeyError: |
|
342 print(f"Command for unknown debugger ID '{debuggerId}' received.") |
|
343 # tell the eric-ide again, that this debugger ID is gone |
|
344 self.__server.sendJson( |
|
345 category=EricRequestCategory.Debugger, |
|
346 reply="DebugClientDisconnected", |
|
347 params={"debugger_id": debuggerId}, |
|
348 ) |
|
349 sock = ( |
|
350 self.__connections[self.__mainClientId] if self.__mainClientId else None |
|
351 ) |
|
352 if sock: |
|
353 self.__server.sendJsonCommand(jsonStr, sock) |