debugger.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745
  1. """Debugger implementation for the IPython kernel."""
  2. import os
  3. import re
  4. import sys
  5. import typing as t
  6. from pathlib import Path
  7. import zmq
  8. from IPython.core.getipython import get_ipython
  9. from IPython.core.inputtransformer2 import leading_empty_lines
  10. from tornado.locks import Event
  11. from tornado.queues import Queue
  12. from zmq.utils import jsonapi
  13. try:
  14. from jupyter_client.jsonutil import json_default
  15. except ImportError:
  16. from jupyter_client.jsonutil import date_default as json_default
  17. from .compiler import get_file_name, get_tmp_directory, get_tmp_hash_seed
  18. try:
  19. # This import is required to have the next ones working...
  20. from debugpy.server import api # noqa: F401
  21. from _pydevd_bundle import pydevd_frame_utils # isort: skip
  22. from _pydevd_bundle.pydevd_suspended_frames import ( # isort: skip
  23. SuspendedFramesManager,
  24. _FramesTracker,
  25. )
  26. _is_debugpy_available = True
  27. except ImportError:
  28. _is_debugpy_available = False
  29. except Exception as e:
  30. # We cannot import the module where the DebuggerInitializationError
  31. # is defined
  32. if e.__class__.__name__ == "DebuggerInitializationError":
  33. _is_debugpy_available = False
  34. else:
  35. raise e
  36. # Required for backwards compatibility
  37. ROUTING_ID = getattr(zmq, "ROUTING_ID", None) or zmq.IDENTITY
  38. class _FakeCode:
  39. """Fake code class."""
  40. def __init__(self, co_filename, co_name):
  41. """Init."""
  42. self.co_filename = co_filename
  43. self.co_name = co_name
  44. class _FakeFrame:
  45. """Fake frame class."""
  46. def __init__(self, f_code, f_globals, f_locals):
  47. """Init."""
  48. self.f_code = f_code
  49. self.f_globals = f_globals
  50. self.f_locals = f_locals
  51. self.f_back = None
  52. class _DummyPyDB:
  53. """Fake PyDb class."""
  54. def __init__(self):
  55. """Init."""
  56. from _pydevd_bundle.pydevd_api import PyDevdAPI
  57. self.variable_presentation = PyDevdAPI.VariablePresentation()
  58. class VariableExplorer:
  59. """A variable explorer."""
  60. def __init__(self):
  61. """Initialize the explorer."""
  62. self.suspended_frame_manager = SuspendedFramesManager()
  63. self.py_db = _DummyPyDB()
  64. self.tracker = _FramesTracker(self.suspended_frame_manager, self.py_db)
  65. self.frame = None
  66. def track(self):
  67. """Start tracking."""
  68. var = get_ipython().user_ns
  69. self.frame = _FakeFrame(_FakeCode("<module>", get_file_name("sys._getframe()")), var, var)
  70. self.tracker.track("thread1", pydevd_frame_utils.create_frames_list_from_frame(self.frame))
  71. def untrack_all(self):
  72. """Stop tracking."""
  73. self.tracker.untrack_all()
  74. def get_children_variables(self, variable_ref=None):
  75. """Get the child variables for a variable reference."""
  76. var_ref = variable_ref
  77. if not var_ref:
  78. var_ref = id(self.frame)
  79. variables = self.suspended_frame_manager.get_variable(var_ref)
  80. return [x.get_var_data() for x in variables.get_children_variables()]
  81. class DebugpyMessageQueue:
  82. """A debugpy message queue."""
  83. HEADER = "Content-Length: "
  84. HEADER_LENGTH = 16
  85. SEPARATOR = "\r\n\r\n"
  86. SEPARATOR_LENGTH = 4
  87. def __init__(self, event_callback, log):
  88. """Init the queue."""
  89. self.tcp_buffer = ""
  90. self._reset_tcp_pos()
  91. self.event_callback = event_callback
  92. self.message_queue: Queue[t.Any] = Queue()
  93. self.log = log
  94. def _reset_tcp_pos(self):
  95. self.header_pos = -1
  96. self.separator_pos = -1
  97. self.message_size = 0
  98. self.message_pos = -1
  99. def _put_message(self, raw_msg):
  100. self.log.debug("QUEUE - _put_message:")
  101. msg = t.cast(dict[str, t.Any], jsonapi.loads(raw_msg))
  102. if msg["type"] == "event":
  103. self.log.debug("QUEUE - received event:")
  104. self.log.debug(msg)
  105. self.event_callback(msg)
  106. else:
  107. self.log.debug("QUEUE - put message:")
  108. self.log.debug(msg)
  109. self.message_queue.put_nowait(msg)
  110. def put_tcp_frame(self, frame):
  111. """Put a tcp frame in the queue."""
  112. self.tcp_buffer += frame
  113. self.log.debug("QUEUE - received frame")
  114. while True:
  115. # Finds header
  116. if self.header_pos == -1:
  117. self.header_pos = self.tcp_buffer.find(DebugpyMessageQueue.HEADER)
  118. if self.header_pos == -1:
  119. return
  120. self.log.debug("QUEUE - found header at pos %i", self.header_pos)
  121. # Finds separator
  122. if self.separator_pos == -1:
  123. hint = self.header_pos + DebugpyMessageQueue.HEADER_LENGTH
  124. self.separator_pos = self.tcp_buffer.find(DebugpyMessageQueue.SEPARATOR, hint)
  125. if self.separator_pos == -1:
  126. return
  127. self.log.debug("QUEUE - found separator at pos %i", self.separator_pos)
  128. if self.message_pos == -1:
  129. size_pos = self.header_pos + DebugpyMessageQueue.HEADER_LENGTH
  130. self.message_pos = self.separator_pos + DebugpyMessageQueue.SEPARATOR_LENGTH
  131. self.message_size = int(self.tcp_buffer[size_pos : self.separator_pos])
  132. self.log.debug("QUEUE - found message at pos %i", self.message_pos)
  133. self.log.debug("QUEUE - message size is %i", self.message_size)
  134. if len(self.tcp_buffer) - self.message_pos < self.message_size:
  135. return
  136. self._put_message(
  137. self.tcp_buffer[self.message_pos : self.message_pos + self.message_size]
  138. )
  139. if len(self.tcp_buffer) - self.message_pos == self.message_size:
  140. self.log.debug("QUEUE - resetting tcp_buffer")
  141. self.tcp_buffer = ""
  142. self._reset_tcp_pos()
  143. return
  144. self.tcp_buffer = self.tcp_buffer[self.message_pos + self.message_size :]
  145. self.log.debug("QUEUE - slicing tcp_buffer: %s", self.tcp_buffer)
  146. self._reset_tcp_pos()
  147. async def get_message(self):
  148. """Get a message from the queue."""
  149. return await self.message_queue.get()
  150. class DebugpyClient:
  151. """A client for debugpy."""
  152. def __init__(self, log, debugpy_stream, event_callback):
  153. """Initialize the client."""
  154. self.log = log
  155. self.debugpy_stream = debugpy_stream
  156. self.event_callback = event_callback
  157. self.message_queue = DebugpyMessageQueue(self._forward_event, self.log)
  158. self.debugpy_host = "127.0.0.1"
  159. self.debugpy_port = -1
  160. self.routing_id = None
  161. self.wait_for_attach = True
  162. self.init_event = Event()
  163. self.init_event_seq = -1
  164. def _get_endpoint(self):
  165. host, port = self.get_host_port()
  166. return "tcp://" + host + ":" + str(port)
  167. def _forward_event(self, msg):
  168. if msg["event"] == "initialized":
  169. self.init_event.set()
  170. self.init_event_seq = msg["seq"]
  171. self.event_callback(msg)
  172. def _send_request(self, msg):
  173. if self.routing_id is None:
  174. self.routing_id = self.debugpy_stream.socket.getsockopt(ROUTING_ID)
  175. content = jsonapi.dumps(
  176. msg,
  177. default=json_default,
  178. ensure_ascii=False,
  179. allow_nan=False,
  180. )
  181. content_length = str(len(content))
  182. buf = (DebugpyMessageQueue.HEADER + content_length + DebugpyMessageQueue.SEPARATOR).encode(
  183. "ascii"
  184. )
  185. buf += content
  186. self.log.debug("DEBUGPYCLIENT:")
  187. self.log.debug(self.routing_id)
  188. self.log.debug(buf)
  189. self.debugpy_stream.send_multipart((self.routing_id, buf))
  190. async def _wait_for_response(self):
  191. # Since events are never pushed to the message_queue
  192. # we can safely assume the next message in queue
  193. # will be an answer to the previous request
  194. return await self.message_queue.get_message()
  195. async def _handle_init_sequence(self):
  196. # 1] Waits for initialized event
  197. await self.init_event.wait()
  198. # 2] Sends configurationDone request
  199. configurationDone = {
  200. "type": "request",
  201. "seq": int(self.init_event_seq) + 1,
  202. "command": "configurationDone",
  203. }
  204. self._send_request(configurationDone)
  205. # 3] Waits for configurationDone response
  206. await self._wait_for_response()
  207. # 4] Waits for attachResponse and returns it
  208. return await self._wait_for_response()
  209. def get_host_port(self):
  210. """Get the host debugpy port."""
  211. if self.debugpy_port == -1:
  212. socket = self.debugpy_stream.socket
  213. socket.bind_to_random_port("tcp://" + self.debugpy_host)
  214. self.endpoint = socket.getsockopt(zmq.LAST_ENDPOINT).decode("utf-8")
  215. socket.unbind(self.endpoint)
  216. index = self.endpoint.rfind(":")
  217. self.debugpy_port = self.endpoint[index + 1 :]
  218. return self.debugpy_host, self.debugpy_port
  219. def connect_tcp_socket(self):
  220. """Connect to the tcp socket."""
  221. self.debugpy_stream.socket.connect(self._get_endpoint())
  222. self.routing_id = self.debugpy_stream.socket.getsockopt(ROUTING_ID)
  223. def disconnect_tcp_socket(self):
  224. """Disconnect from the tcp socket."""
  225. self.debugpy_stream.socket.disconnect(self._get_endpoint())
  226. self.routing_id = None
  227. self.init_event = Event()
  228. self.init_event_seq = -1
  229. self.wait_for_attach = True
  230. def receive_dap_frame(self, frame):
  231. """Receive a dap frame."""
  232. self.message_queue.put_tcp_frame(frame)
  233. async def send_dap_request(self, msg):
  234. """Send a dap request."""
  235. self._send_request(msg)
  236. if self.wait_for_attach and msg["command"] == "attach":
  237. rep = await self._handle_init_sequence()
  238. self.wait_for_attach = False
  239. return rep
  240. rep = await self._wait_for_response()
  241. self.log.debug("DEBUGPYCLIENT - returning:")
  242. self.log.debug(rep)
  243. return rep
  244. class Debugger:
  245. """The debugger class."""
  246. # Requests that requires that the debugger has started
  247. started_debug_msg_types = [
  248. "dumpCell",
  249. "setBreakpoints",
  250. "source",
  251. "stackTrace",
  252. "variables",
  253. "attach",
  254. "configurationDone",
  255. ]
  256. # Requests that can be handled even if the debugger is not running
  257. static_debug_msg_types = [
  258. "debugInfo",
  259. "inspectVariables",
  260. "richInspectVariables",
  261. "modules",
  262. "copyToGlobals",
  263. ]
  264. def __init__(
  265. self,
  266. log,
  267. debugpy_stream,
  268. event_callback,
  269. shell_socket,
  270. session,
  271. kernel_modules,
  272. just_my_code=False,
  273. filter_internal_frames=True,
  274. ):
  275. """Initialize the debugger."""
  276. self.log = log
  277. self.debugpy_client = DebugpyClient(log, debugpy_stream, self._handle_event)
  278. self.shell_socket = shell_socket
  279. self.session = session
  280. self.is_started = False
  281. self.event_callback = event_callback
  282. self.kernel_modules = kernel_modules
  283. self.just_my_code = just_my_code
  284. self.filter_internal_frames = filter_internal_frames
  285. self.stopped_queue: Queue[t.Any] = Queue()
  286. self.started_debug_handlers = {}
  287. for msg_type in Debugger.started_debug_msg_types:
  288. self.started_debug_handlers[msg_type] = getattr(self, msg_type)
  289. self.static_debug_handlers = {}
  290. for msg_type in Debugger.static_debug_msg_types:
  291. self.static_debug_handlers[msg_type] = getattr(self, msg_type)
  292. self.breakpoint_list = {}
  293. self.stopped_threads = set()
  294. self.debugpy_initialized = False
  295. self._removed_cleanup = {}
  296. self.debugpy_host = "127.0.0.1"
  297. self.debugpy_port = 0
  298. self.endpoint = None
  299. self.variable_explorer = VariableExplorer()
  300. def _handle_event(self, msg):
  301. if msg["event"] == "stopped":
  302. if msg["body"]["allThreadsStopped"]:
  303. self.stopped_queue.put_nowait(msg)
  304. # Do not forward the event now, will be done in the handle_stopped_event
  305. return
  306. self.stopped_threads.add(msg["body"]["threadId"])
  307. self.event_callback(msg)
  308. elif msg["event"] == "continued":
  309. if msg["body"]["allThreadsContinued"]:
  310. self.stopped_threads = set()
  311. else:
  312. self.stopped_threads.remove(msg["body"]["threadId"])
  313. self.event_callback(msg)
  314. else:
  315. self.event_callback(msg)
  316. async def _forward_message(self, msg):
  317. return await self.debugpy_client.send_dap_request(msg)
  318. def _build_variables_response(self, request, variables):
  319. var_list = [var for var in variables if self.accept_variable(var["name"])]
  320. return {
  321. "seq": request["seq"],
  322. "type": "response",
  323. "request_seq": request["seq"],
  324. "success": True,
  325. "command": request["command"],
  326. "body": {"variables": var_list},
  327. }
  328. def _accept_stopped_thread(self, thread_name):
  329. # TODO: identify Thread-2, Thread-3 and Thread-4. These are NOT
  330. # Control, IOPub or Heartbeat threads
  331. forbid_list = ["IPythonHistorySavingThread", "Thread-2", "Thread-3", "Thread-4"]
  332. return thread_name not in forbid_list
  333. async def handle_stopped_event(self):
  334. """Handle a stopped event."""
  335. # Wait for a stopped event message in the stopped queue
  336. # This message is used for triggering the 'threads' request
  337. event = await self.stopped_queue.get()
  338. req = {"seq": event["seq"] + 1, "type": "request", "command": "threads"}
  339. rep = await self._forward_message(req)
  340. for thread in rep["body"]["threads"]:
  341. if self._accept_stopped_thread(thread["name"]):
  342. self.stopped_threads.add(thread["id"])
  343. self.event_callback(event)
  344. @property
  345. def tcp_client(self):
  346. return self.debugpy_client
  347. def start(self):
  348. """Start the debugger."""
  349. if not self.debugpy_initialized:
  350. tmp_dir = get_tmp_directory()
  351. if not Path(tmp_dir).exists():
  352. Path(tmp_dir).mkdir(parents=True)
  353. host, port = self.debugpy_client.get_host_port()
  354. code = "import debugpy;"
  355. code += 'debugpy.listen(("' + host + '",' + port + "))"
  356. content = {"code": code, "silent": True}
  357. self.session.send(
  358. self.shell_socket,
  359. "execute_request",
  360. content,
  361. None,
  362. (self.shell_socket.getsockopt(ROUTING_ID)),
  363. )
  364. _ident, msg = self.session.recv(self.shell_socket, mode=0)
  365. self.debugpy_initialized = msg["content"]["status"] == "ok"
  366. # Don't remove leading empty lines when debugging so the breakpoints are correctly positioned
  367. cleanup_transforms = get_ipython().input_transformer_manager.cleanup_transforms
  368. if leading_empty_lines in cleanup_transforms:
  369. index = cleanup_transforms.index(leading_empty_lines)
  370. self._removed_cleanup[index] = cleanup_transforms.pop(index)
  371. self.debugpy_client.connect_tcp_socket()
  372. return self.debugpy_initialized
  373. def stop(self):
  374. """Stop the debugger."""
  375. self.debugpy_client.disconnect_tcp_socket()
  376. # Restore remove cleanup transformers
  377. cleanup_transforms = get_ipython().input_transformer_manager.cleanup_transforms
  378. for index in sorted(self._removed_cleanup):
  379. func = self._removed_cleanup.pop(index)
  380. cleanup_transforms.insert(index, func)
  381. async def dumpCell(self, message):
  382. """Handle a dump cell message."""
  383. code = message["arguments"]["code"]
  384. file_name = get_file_name(code)
  385. with open(file_name, "w", encoding="utf-8") as f:
  386. f.write(code)
  387. return {
  388. "type": "response",
  389. "request_seq": message["seq"],
  390. "success": True,
  391. "command": message["command"],
  392. "body": {"sourcePath": file_name},
  393. }
  394. async def setBreakpoints(self, message):
  395. """Handle a set breakpoints message."""
  396. source = message["arguments"]["source"]["path"]
  397. self.breakpoint_list[source] = message["arguments"]["breakpoints"]
  398. message_response = await self._forward_message(message)
  399. # debugpy can set breakpoints on different lines than the ones requested,
  400. # so we want to record the breakpoints that were actually added
  401. if message_response.get("success"):
  402. self.breakpoint_list[source] = [
  403. {"line": breakpoint["line"]}
  404. for breakpoint in message_response["body"]["breakpoints"]
  405. ]
  406. return message_response
  407. async def source(self, message):
  408. """Handle a source message."""
  409. reply = {"type": "response", "request_seq": message["seq"], "command": message["command"]}
  410. source_path = message["arguments"]["source"]["path"]
  411. if Path(source_path).is_file():
  412. with open(source_path, encoding="utf-8") as f:
  413. reply["success"] = True
  414. reply["body"] = {"content": f.read()}
  415. else:
  416. reply["success"] = False
  417. reply["message"] = "source unavailable"
  418. reply["body"] = {}
  419. return reply
  420. async def stackTrace(self, message):
  421. """Handle a stack trace message."""
  422. return await self._forward_message(message)
  423. def accept_variable(self, variable_name):
  424. """Accept a variable by name."""
  425. forbid_list = [
  426. "__name__",
  427. "__doc__",
  428. "__package__",
  429. "__loader__",
  430. "__spec__",
  431. "__annotations__",
  432. "__builtins__",
  433. "__builtin__",
  434. "__display__",
  435. "get_ipython",
  436. "debugpy",
  437. "exit",
  438. "quit",
  439. "In",
  440. "Out",
  441. "_oh",
  442. "_dh",
  443. "_",
  444. "__",
  445. "___",
  446. ]
  447. cond = variable_name not in forbid_list
  448. cond = cond and not bool(re.search(r"^_\d", variable_name))
  449. cond = cond and variable_name[0:2] != "_i"
  450. return cond # noqa: RET504
  451. async def variables(self, message):
  452. """Handle a variables message."""
  453. reply = {}
  454. if not self.stopped_threads:
  455. variables = self.variable_explorer.get_children_variables(
  456. message["arguments"]["variablesReference"]
  457. )
  458. return self._build_variables_response(message, variables)
  459. reply = await self._forward_message(message)
  460. # TODO : check start and count arguments work as expected in debugpy
  461. reply["body"]["variables"] = [
  462. var for var in reply["body"]["variables"] if self.accept_variable(var["name"])
  463. ]
  464. return reply
  465. async def attach(self, message):
  466. """Handle an attach message."""
  467. host, port = self.debugpy_client.get_host_port()
  468. message["arguments"]["connect"] = {"host": host, "port": port}
  469. message["arguments"]["logToFile"] = True
  470. # Experimental option to break in non-user code.
  471. # The ipykernel source is in the call stack, so the user
  472. # has to manipulate the step-over and step-into in a wize way.
  473. # Set debugOptions for breakpoints in python standard library source.
  474. if not self.just_my_code:
  475. message["arguments"]["debugOptions"] = ["DebugStdLib"]
  476. # Dynamic skip rules (computed at kernel startup)
  477. if self.filter_internal_frames:
  478. rules = [{"path": path, "include": False} for path in self.kernel_modules]
  479. message["arguments"]["rules"] = rules
  480. return await self._forward_message(message)
  481. async def configurationDone(self, message):
  482. """Handle a configuration done message."""
  483. return {
  484. "seq": message["seq"],
  485. "type": "response",
  486. "request_seq": message["seq"],
  487. "success": True,
  488. "command": message["command"],
  489. }
  490. async def debugInfo(self, message):
  491. """Handle a debug info message."""
  492. breakpoint_list = []
  493. for key, value in self.breakpoint_list.items():
  494. breakpoint_list.append({"source": key, "breakpoints": value})
  495. return {
  496. "type": "response",
  497. "request_seq": message["seq"],
  498. "success": True,
  499. "command": message["command"],
  500. "body": {
  501. "isStarted": self.is_started,
  502. "hashMethod": "Murmur2",
  503. "hashSeed": get_tmp_hash_seed(),
  504. "tmpFilePrefix": get_tmp_directory() + os.sep,
  505. "tmpFileSuffix": ".py",
  506. "breakpoints": breakpoint_list,
  507. "stoppedThreads": list(self.stopped_threads),
  508. "richRendering": True,
  509. "exceptionPaths": ["Python Exceptions"],
  510. "copyToGlobals": True,
  511. },
  512. }
  513. async def inspectVariables(self, message):
  514. """Handle an inspect variables message."""
  515. self.variable_explorer.untrack_all()
  516. # looks like the implementation of untrack_all in ptvsd
  517. # destroys objects we nee din track. We have no choice but
  518. # reinstantiate the object
  519. self.variable_explorer = VariableExplorer()
  520. self.variable_explorer.track()
  521. variables = self.variable_explorer.get_children_variables()
  522. return self._build_variables_response(message, variables)
  523. async def richInspectVariables(self, message):
  524. """Handle a rich inspect variables message."""
  525. reply = {
  526. "type": "response",
  527. "sequence_seq": message["seq"],
  528. "success": False,
  529. "command": message["command"],
  530. }
  531. var_name = message["arguments"]["variableName"]
  532. valid_name = str.isidentifier(var_name)
  533. if not valid_name:
  534. reply["body"] = {"data": {}, "metadata": {}}
  535. if var_name == "special variables" or var_name == "function variables":
  536. reply["success"] = True
  537. return reply
  538. repr_data = {}
  539. repr_metadata = {}
  540. if not self.stopped_threads:
  541. # The code did not hit a breakpoint, we use the interpreter
  542. # to get the rich representation of the variable
  543. result = get_ipython().user_expressions({var_name: var_name})[var_name]
  544. if result.get("status", "error") == "ok":
  545. repr_data = result.get("data", {})
  546. repr_metadata = result.get("metadata", {})
  547. else:
  548. # The code has stopped on a breakpoint, we use the setExpression
  549. # request to get the rich representation of the variable
  550. code = f"get_ipython().display_formatter.format({var_name})"
  551. frame_id = message["arguments"]["frameId"]
  552. seq = message["seq"]
  553. reply = await self._forward_message(
  554. {
  555. "type": "request",
  556. "command": "evaluate",
  557. "seq": seq + 1,
  558. "arguments": {"expression": code, "frameId": frame_id, "context": "clipboard"},
  559. }
  560. )
  561. if reply["success"]:
  562. repr_data, repr_metadata = eval(reply["body"]["result"], {}, {})
  563. body = {
  564. "data": repr_data,
  565. "metadata": {k: v for k, v in repr_metadata.items() if k in repr_data},
  566. }
  567. reply["body"] = body
  568. reply["success"] = True
  569. return reply
  570. async def copyToGlobals(self, message):
  571. dst_var_name = message["arguments"]["dstVariableName"]
  572. src_var_name = message["arguments"]["srcVariableName"]
  573. src_frame_id = message["arguments"]["srcFrameId"]
  574. expression = f"globals()['{dst_var_name}']"
  575. seq = message["seq"]
  576. return await self._forward_message(
  577. {
  578. "type": "request",
  579. "command": "setExpression",
  580. "seq": seq + 1,
  581. "arguments": {
  582. "expression": expression,
  583. "value": src_var_name,
  584. "frameId": src_frame_id,
  585. },
  586. }
  587. )
  588. async def modules(self, message):
  589. """Handle a modules message."""
  590. modules = list(sys.modules.values())
  591. startModule = message.get("startModule", 0)
  592. moduleCount = message.get("moduleCount", len(modules))
  593. mods = []
  594. for i in range(startModule, moduleCount):
  595. module = modules[i]
  596. filename = getattr(getattr(module, "__spec__", None), "origin", None)
  597. if filename and filename.endswith(".py"):
  598. mods.append({"id": i, "name": module.__name__, "path": filename})
  599. return {"body": {"modules": mods, "totalModules": len(modules)}}
  600. async def process_request(self, message):
  601. """Process a request."""
  602. reply = {}
  603. if message["command"] == "initialize":
  604. if self.is_started:
  605. self.log.info("The debugger has already started")
  606. else:
  607. self.is_started = self.start()
  608. if self.is_started:
  609. self.log.info("The debugger has started")
  610. else:
  611. reply = {
  612. "command": "initialize",
  613. "request_seq": message["seq"],
  614. "seq": 3,
  615. "success": False,
  616. "type": "response",
  617. }
  618. handler = self.static_debug_handlers.get(message["command"], None)
  619. if handler is not None:
  620. reply = await handler(message)
  621. elif self.is_started:
  622. handler = self.started_debug_handlers.get(message["command"], None)
  623. if handler is not None:
  624. reply = await handler(message)
  625. else:
  626. reply = await self._forward_message(message)
  627. if message["command"] == "disconnect":
  628. self.stop()
  629. self.breakpoint_list = {}
  630. self.stopped_threads = set()
  631. self.is_started = False
  632. self.log.info("The debugger has stopped")
  633. return reply