src/eric7/RemoteServer/EricServerDebuggerRequestHandler.py

branch
server
changeset 10555
08e853c0c77b
child 10561
be23a662d709
equal deleted inserted replaced
10551:d80184d38152 10555:08e853c0c77b
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)

eric ide

mercurial