clients.py 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796
  1. # Copyright (c) Microsoft Corporation. All rights reserved.
  2. # Licensed under the MIT License. See LICENSE in the project root
  3. # for license information.
  4. from __future__ import annotations
  5. import atexit
  6. import os
  7. import sys
  8. import debugpy
  9. from debugpy import adapter, common, launcher
  10. from debugpy.common import json, log, messaging, sockets
  11. from debugpy.adapter import clients, components, launchers, servers, sessions
  12. class Client(components.Component):
  13. """Handles the client side of a debug session."""
  14. message_handler = components.Component.message_handler
  15. known_subprocesses: set[servers.Connection]
  16. """Server connections to subprocesses that this client has been made aware of.
  17. """
  18. class Capabilities(components.Capabilities):
  19. PROPERTIES = {
  20. "supportsVariableType": False,
  21. "supportsVariablePaging": False,
  22. "supportsRunInTerminalRequest": False,
  23. "supportsMemoryReferences": False,
  24. "supportsArgsCanBeInterpretedByShell": False,
  25. "supportsStartDebuggingRequest": False,
  26. }
  27. class Expectations(components.Capabilities):
  28. PROPERTIES = {
  29. "locale": "en-US",
  30. "linesStartAt1": True,
  31. "columnsStartAt1": True,
  32. "pathFormat": json.enum("path", optional=True), # we don't support "uri"
  33. }
  34. def __init__(self, sock):
  35. if sock == "stdio":
  36. log.info("Connecting to client over stdio...", self)
  37. self.using_stdio = True
  38. stream = messaging.JsonIOStream.from_stdio()
  39. # Make sure that nothing else tries to interfere with the stdio streams
  40. # that are going to be used for DAP communication from now on.
  41. sys.stdin = stdin = open(os.devnull, "r")
  42. atexit.register(stdin.close)
  43. sys.stdout = stdout = open(os.devnull, "w")
  44. atexit.register(stdout.close)
  45. else:
  46. self.using_stdio = False
  47. stream = messaging.JsonIOStream.from_socket(sock)
  48. with sessions.Session() as session:
  49. super().__init__(session, stream)
  50. self.client_id = None
  51. """ID of the connecting client. This can be 'test' while running tests."""
  52. self.has_started = False
  53. """Whether the "launch" or "attach" request was received from the client, and
  54. fully handled.
  55. """
  56. self.start_request = None
  57. """The "launch" or "attach" request as received from the client.
  58. """
  59. self.restart_requested = False
  60. """Whether the client requested the debug adapter to be automatically
  61. restarted via "restart":true in the start request.
  62. """
  63. self._initialize_request = None
  64. """The "initialize" request as received from the client, to propagate to the
  65. server later."""
  66. self._deferred_events = []
  67. """Deferred events from the launcher and the server that must be propagated
  68. only if and when the "launch" or "attach" response is sent.
  69. """
  70. self._forward_terminate_request = False
  71. self.known_subprocesses = set()
  72. session.client = self
  73. session.register()
  74. # For the transition period, send the telemetry events with both old and new
  75. # name. The old one should be removed once the new one lights up.
  76. self.channel.send_event(
  77. "output",
  78. {
  79. "category": "telemetry",
  80. "output": "ptvsd",
  81. "data": {"packageVersion": debugpy.__version__},
  82. },
  83. )
  84. self.channel.send_event(
  85. "output",
  86. {
  87. "category": "telemetry",
  88. "output": "debugpy",
  89. "data": {"packageVersion": debugpy.__version__},
  90. },
  91. )
  92. sessions.report_sockets()
  93. def propagate_after_start(self, event):
  94. # pydevd starts sending events as soon as we connect, but the client doesn't
  95. # expect to see any until it receives the response to "launch" or "attach"
  96. # request. If client is not ready yet, save the event instead of propagating
  97. # it immediately.
  98. if self._deferred_events is not None:
  99. self._deferred_events.append(event)
  100. log.debug("Propagation deferred.")
  101. else:
  102. self.client.channel.propagate(event)
  103. def _propagate_deferred_events(self):
  104. log.debug("Propagating deferred events to {0}...", self.client)
  105. for event in self._deferred_events:
  106. log.debug("Propagating deferred {0}", event.describe())
  107. self.client.channel.propagate(event)
  108. log.info("All deferred events propagated to {0}.", self.client)
  109. self._deferred_events = None
  110. # Generic event handler. There are no specific handlers for client events, because
  111. # there are no events from the client in DAP - but we propagate them if we can, in
  112. # case some events appear in future protocol versions.
  113. @message_handler
  114. def event(self, event):
  115. if self.server:
  116. self.server.channel.propagate(event)
  117. # Generic request handler, used if there's no specific handler below.
  118. @message_handler
  119. def request(self, request):
  120. return self.server.channel.delegate(request)
  121. @message_handler
  122. def initialize_request(self, request):
  123. if self._initialize_request is not None:
  124. raise request.isnt_valid("Session is already initialized")
  125. self.client_id = request("clientID", "")
  126. self.capabilities = self.Capabilities(self, request)
  127. self.expectations = self.Expectations(self, request)
  128. self._initialize_request = request
  129. exception_breakpoint_filters = [
  130. {
  131. "filter": "raised",
  132. "label": "Raised Exceptions",
  133. "default": False,
  134. "description": "Break whenever any exception is raised.",
  135. },
  136. {
  137. "filter": "uncaught",
  138. "label": "Uncaught Exceptions",
  139. "default": True,
  140. "description": "Break when the process is exiting due to unhandled exception.",
  141. },
  142. {
  143. "filter": "userUnhandled",
  144. "label": "User Uncaught Exceptions",
  145. "default": False,
  146. "description": "Break when exception escapes into library code.",
  147. },
  148. ]
  149. return {
  150. "supportsCompletionsRequest": True,
  151. "supportsConditionalBreakpoints": True,
  152. "supportsConfigurationDoneRequest": True,
  153. "supportsDebuggerProperties": True,
  154. "supportsDelayedStackTraceLoading": True,
  155. "supportsEvaluateForHovers": True,
  156. "supportsExceptionInfoRequest": True,
  157. "supportsExceptionOptions": True,
  158. "supportsFunctionBreakpoints": True,
  159. "supportsHitConditionalBreakpoints": True,
  160. "supportsLogPoints": True,
  161. "supportsModulesRequest": True,
  162. "supportsSetExpression": True,
  163. "supportsSetVariable": True,
  164. "supportsValueFormattingOptions": True,
  165. "supportsTerminateDebuggee": True,
  166. "supportsTerminateRequest": True,
  167. "supportsGotoTargetsRequest": True,
  168. "supportsClipboardContext": True,
  169. "exceptionBreakpointFilters": exception_breakpoint_filters,
  170. "supportsStepInTargetsRequest": True,
  171. }
  172. # Common code for "launch" and "attach" request handlers.
  173. #
  174. # See https://github.com/microsoft/vscode/issues/4902#issuecomment-368583522
  175. # for the sequence of request and events necessary to orchestrate the start.
  176. def _start_message_handler(f):
  177. @components.Component.message_handler
  178. def handle(self, request):
  179. assert request.is_request("launch", "attach")
  180. if self._initialize_request is None:
  181. raise request.isnt_valid("Session is not initialized yet")
  182. if self.launcher or self.server:
  183. raise request.isnt_valid("Session is already started")
  184. self.session.no_debug = request("noDebug", json.default(False))
  185. if self.session.no_debug:
  186. servers.dont_wait_for_first_connection()
  187. self.session.debug_options = debug_options = set(
  188. request("debugOptions", json.array(str))
  189. )
  190. f(self, request)
  191. if request.response is not None:
  192. return
  193. if self.server:
  194. self.server.initialize(self._initialize_request)
  195. self._initialize_request = None
  196. arguments = request.arguments
  197. if self.launcher:
  198. redirecting = arguments.get("console") == "internalConsole"
  199. if "RedirectOutput" in debug_options:
  200. # The launcher is doing output redirection, so we don't need the
  201. # server to do it, as well.
  202. arguments = dict(arguments)
  203. arguments["debugOptions"] = list(
  204. debug_options - {"RedirectOutput"}
  205. )
  206. redirecting = True
  207. if arguments.get("redirectOutput"):
  208. arguments = dict(arguments)
  209. del arguments["redirectOutput"]
  210. redirecting = True
  211. arguments["isOutputRedirected"] = redirecting
  212. # pydevd doesn't send "initialized", and responds to the start request
  213. # immediately, without waiting for "configurationDone". If it changes
  214. # to conform to the DAP spec, we'll need to defer waiting for response.
  215. try:
  216. self.server.channel.request(request.command, arguments)
  217. except messaging.NoMoreMessages:
  218. # Server closed connection before we could receive the response to
  219. # "attach" or "launch" - this can happen when debuggee exits shortly
  220. # after starting. It's not an error, but we can't do anything useful
  221. # here at this point, either, so just bail out.
  222. request.respond({})
  223. self.session.finalize(
  224. "{0} disconnected before responding to {1}".format(
  225. self.server,
  226. json.repr(request.command),
  227. )
  228. )
  229. return
  230. except messaging.MessageHandlingError as exc:
  231. exc.propagate(request)
  232. if self.session.no_debug:
  233. self.start_request = request
  234. self.has_started = True
  235. request.respond({})
  236. self._propagate_deferred_events()
  237. return
  238. # Let the client know that it can begin configuring the adapter.
  239. self.channel.send_event("initialized")
  240. self.start_request = request
  241. return messaging.NO_RESPONSE # will respond on "configurationDone"
  242. return handle
  243. @_start_message_handler
  244. def launch_request(self, request):
  245. from debugpy.adapter import launchers
  246. if self.session.id != 1 or len(servers.connections()):
  247. raise request.cant_handle('"attach" expected')
  248. debug_options = set(request("debugOptions", json.array(str)))
  249. # Handling of properties that can also be specified as legacy "debugOptions" flags.
  250. # If property is explicitly set to false, but the flag is in "debugOptions", treat
  251. # it as an error. Returns None if the property wasn't explicitly set either way.
  252. def property_or_debug_option(prop_name, flag_name):
  253. assert prop_name[0].islower() and flag_name[0].isupper()
  254. value = request(prop_name, bool, optional=True)
  255. if value == ():
  256. value = None
  257. if flag_name in debug_options:
  258. if value is False:
  259. raise request.isnt_valid(
  260. '{0}:false and "debugOptions":[{1}] are mutually exclusive',
  261. json.repr(prop_name),
  262. json.repr(flag_name),
  263. )
  264. value = True
  265. return value
  266. # "pythonPath" is a deprecated legacy spelling. If "python" is missing, then try
  267. # the alternative. But if both are missing, the error message should say "python".
  268. python_key = "python"
  269. if python_key in request:
  270. if "pythonPath" in request:
  271. raise request.isnt_valid(
  272. '"pythonPath" is not valid if "python" is specified'
  273. )
  274. elif "pythonPath" in request:
  275. python_key = "pythonPath"
  276. python = request(python_key, json.array(str, vectorize=True, size=(0,)))
  277. if not len(python):
  278. python = [sys.executable]
  279. python += request("pythonArgs", json.array(str, size=(0,)))
  280. request.arguments["pythonArgs"] = python[1:]
  281. request.arguments["python"] = python
  282. launcher_python = request("debugLauncherPython", str, optional=True)
  283. if launcher_python == ():
  284. launcher_python = python[0]
  285. program = module = code = ()
  286. if "program" in request:
  287. program = request("program", str)
  288. args = [program]
  289. request.arguments["processName"] = program
  290. if "module" in request:
  291. module = request("module", str)
  292. args = ["-m", module]
  293. request.arguments["processName"] = module
  294. if "code" in request:
  295. code = request("code", json.array(str, vectorize=True, size=(1,)))
  296. args = ["-c", "\n".join(code)]
  297. request.arguments["processName"] = "-c"
  298. num_targets = len([x for x in (program, module, code) if x != ()])
  299. if num_targets == 0:
  300. raise request.isnt_valid(
  301. 'either "program", "module", or "code" must be specified'
  302. )
  303. elif num_targets != 1:
  304. raise request.isnt_valid(
  305. '"program", "module", and "code" are mutually exclusive'
  306. )
  307. console = request(
  308. "console",
  309. json.enum(
  310. "internalConsole",
  311. "integratedTerminal",
  312. "externalTerminal",
  313. optional=True,
  314. ),
  315. )
  316. console_title = request("consoleTitle", json.default("Python Debug Console"))
  317. # Propagate "args" via CLI so that shell expansion can be applied if requested.
  318. target_args = request("args", json.array(str, vectorize=True))
  319. args += target_args
  320. # If "args" was a single string rather than an array, shell expansion must be applied.
  321. shell_expand_args = len(target_args) > 0 and isinstance(
  322. request.arguments["args"], str
  323. )
  324. if shell_expand_args:
  325. if not self.capabilities["supportsArgsCanBeInterpretedByShell"]:
  326. raise request.isnt_valid(
  327. 'Shell expansion in "args" is not supported by the client'
  328. )
  329. if console == "internalConsole":
  330. raise request.isnt_valid(
  331. 'Shell expansion in "args" is not available for "console":"internalConsole"'
  332. )
  333. cwd = request("cwd", str, optional=True)
  334. if cwd == ():
  335. # If it's not specified, but we're launching a file rather than a module,
  336. # and the specified path has a directory in it, use that.
  337. cwd = None if program == () else (os.path.dirname(program) or None)
  338. sudo = bool(property_or_debug_option("sudo", "Sudo"))
  339. if sudo and sys.platform == "win32":
  340. raise request.cant_handle('"sudo":true is not supported on Windows.')
  341. on_terminate = request("onTerminate", str, optional=True)
  342. if on_terminate:
  343. self._forward_terminate_request = on_terminate == "KeyboardInterrupt"
  344. launcher_path = request("debugLauncherPath", os.path.dirname(launcher.__file__))
  345. localhost = sockets.get_default_localhost()
  346. adapter_host = request("debugAdapterHost", localhost)
  347. try:
  348. servers.serve(adapter_host)
  349. except Exception as exc:
  350. raise request.cant_handle(
  351. "{0} couldn't create listener socket for servers: {1}",
  352. self.session,
  353. exc,
  354. )
  355. launchers.spawn_debuggee(
  356. self.session,
  357. request,
  358. [launcher_python],
  359. launcher_path,
  360. adapter_host,
  361. args,
  362. shell_expand_args,
  363. cwd,
  364. console,
  365. console_title,
  366. sudo,
  367. )
  368. @_start_message_handler
  369. def attach_request(self, request):
  370. if self.session.no_debug:
  371. raise request.isnt_valid('"noDebug" is not supported for "attach"')
  372. host = request("host", str, optional=True)
  373. port = request("port", int, optional=True)
  374. listen = request("listen", dict, optional=True)
  375. connect = request("connect", dict, optional=True)
  376. pid = request("processId", (int, str), optional=True)
  377. sub_pid = request("subProcessId", int, optional=True)
  378. on_terminate = request("onTerminate", bool, optional=True)
  379. if on_terminate:
  380. self._forward_terminate_request = on_terminate == "KeyboardInterrupt"
  381. if host != () or port != ():
  382. if listen != ():
  383. raise request.isnt_valid(
  384. '"listen" and "host"/"port" are mutually exclusive'
  385. )
  386. if connect != ():
  387. raise request.isnt_valid(
  388. '"connect" and "host"/"port" are mutually exclusive'
  389. )
  390. if listen != ():
  391. if connect != ():
  392. raise request.isnt_valid(
  393. '"listen" and "connect" are mutually exclusive'
  394. )
  395. if pid != ():
  396. raise request.isnt_valid(
  397. '"listen" and "processId" are mutually exclusive'
  398. )
  399. if sub_pid != ():
  400. raise request.isnt_valid(
  401. '"listen" and "subProcessId" are mutually exclusive'
  402. )
  403. if pid != () and sub_pid != ():
  404. raise request.isnt_valid(
  405. '"processId" and "subProcessId" are mutually exclusive'
  406. )
  407. localhost = sockets.get_default_localhost()
  408. if listen != ():
  409. if servers.is_serving():
  410. raise request.isnt_valid(
  411. 'Multiple concurrent "listen" sessions are not supported'
  412. )
  413. host = listen("host", localhost)
  414. port = listen("port", int)
  415. adapter.access_token = None
  416. self.restart_requested = request("restart", False)
  417. host, port = servers.serve(host, port)
  418. else:
  419. if not servers.is_serving():
  420. servers.serve(localhost)
  421. host, port = sockets.get_address(servers.listener)
  422. # There are four distinct possibilities here.
  423. #
  424. # If "processId" is specified, this is attach-by-PID. We need to inject the
  425. # debug server into the designated process, and then wait until it connects
  426. # back to us. Since the injected server can crash, there must be a timeout.
  427. #
  428. # If "subProcessId" is specified, this is attach to a known subprocess, likely
  429. # in response to a "debugpyAttach" event. If so, the debug server should be
  430. # connected already, and thus the wait timeout is zero.
  431. #
  432. # If "listen" is specified, this is attach-by-socket with the server expected
  433. # to connect to the adapter via debugpy.connect(). There is no PID known in
  434. # advance, so just wait until the first server connection indefinitely, with
  435. # no timeout.
  436. #
  437. # If "connect" is specified, this is attach-by-socket in which the server has
  438. # spawned the adapter via debugpy.listen(). There is no PID known to the client
  439. # in advance, but the server connection should be either be there already, or
  440. # the server should be connecting shortly, so there must be a timeout.
  441. #
  442. # In the last two cases, if there's more than one server connection already,
  443. # this is a multiprocess re-attach. The client doesn't know the PID, so we just
  444. # connect it to the oldest server connection that we have - in most cases, it
  445. # will be the one for the root debuggee process, but if it has exited already,
  446. # it will be some subprocess.
  447. if pid != ():
  448. if not isinstance(pid, int):
  449. try:
  450. pid = int(pid)
  451. except Exception:
  452. raise request.isnt_valid('"processId" must be parseable as int')
  453. debugpy_args = request("debugpyArgs", json.array(str))
  454. def on_output(category, output):
  455. self.channel.send_event(
  456. "output",
  457. {
  458. "category": category,
  459. "output": output,
  460. },
  461. )
  462. try:
  463. servers.inject(pid, debugpy_args, on_output)
  464. except Exception as e:
  465. log.swallow_exception()
  466. self.session.finalize(
  467. "Error when trying to attach to PID:\n%s" % (str(e),)
  468. )
  469. return
  470. timeout = common.PROCESS_SPAWN_TIMEOUT
  471. pred = lambda conn: conn.pid == pid
  472. else:
  473. if sub_pid == ():
  474. pred = lambda conn: True
  475. timeout = common.PROCESS_SPAWN_TIMEOUT if listen == () else None
  476. else:
  477. pred = lambda conn: conn.pid == sub_pid
  478. timeout = 0
  479. self.channel.send_event("debugpyWaitingForServer", {"host": host, "port": port})
  480. conn = servers.wait_for_connection(self.session, pred, timeout)
  481. if conn is None:
  482. if sub_pid != ():
  483. # If we can't find a matching subprocess, it's not always an error -
  484. # it might have already exited, or didn't even get a chance to connect.
  485. # To prevent the client from complaining, pretend that the "attach"
  486. # request was successful, but that the session terminated immediately.
  487. request.respond({})
  488. self.session.finalize(
  489. 'No known subprocess with "subProcessId":{0}'.format(sub_pid)
  490. )
  491. return
  492. raise request.cant_handle(
  493. (
  494. "Timed out waiting for debug server to connect."
  495. if timeout
  496. else "There is no debug server connected to this adapter."
  497. ),
  498. sub_pid,
  499. )
  500. try:
  501. conn.attach_to_session(self.session)
  502. except ValueError:
  503. request.cant_handle("{0} is already being debugged.", conn)
  504. @message_handler
  505. def configurationDone_request(self, request):
  506. if self.start_request is None or self.has_started:
  507. request.cant_handle(
  508. '"configurationDone" is only allowed during handling of a "launch" '
  509. 'or an "attach" request'
  510. )
  511. try:
  512. self.has_started = True
  513. try:
  514. result = self.server.channel.delegate(request)
  515. except messaging.NoMoreMessages:
  516. # Server closed connection before we could receive the response to
  517. # "configurationDone" - this can happen when debuggee exits shortly
  518. # after starting. It's not an error, but we can't do anything useful
  519. # here at this point, either, so just bail out.
  520. request.respond({})
  521. self.start_request.respond({})
  522. self.session.finalize(
  523. "{0} disconnected before responding to {1}".format(
  524. self.server,
  525. json.repr(request.command),
  526. )
  527. )
  528. return
  529. else:
  530. request.respond(result)
  531. except messaging.MessageHandlingError as exc:
  532. self.start_request.cant_handle(str(exc))
  533. finally:
  534. if self.start_request.response is None:
  535. self.start_request.respond({})
  536. self._propagate_deferred_events()
  537. # Notify the client of any child processes of the debuggee that aren't already
  538. # being debugged.
  539. for conn in servers.connections():
  540. if conn.server is None and conn.ppid == self.session.pid:
  541. self.notify_of_subprocess(conn)
  542. @message_handler
  543. def evaluate_request(self, request):
  544. propagated_request = self.server.channel.propagate(request)
  545. def handle_response(response):
  546. request.respond(response.body)
  547. propagated_request.on_response(handle_response)
  548. return messaging.NO_RESPONSE
  549. @message_handler
  550. def pause_request(self, request):
  551. request.arguments["threadId"] = "*"
  552. return self.server.channel.delegate(request)
  553. @message_handler
  554. def continue_request(self, request):
  555. request.arguments["threadId"] = "*"
  556. try:
  557. return self.server.channel.delegate(request)
  558. except messaging.NoMoreMessages:
  559. # pydevd can sometimes allow the debuggee to exit before the queued
  560. # "continue" response gets sent. Thus, a failed "continue" response
  561. # indicating that the server disconnected should be treated as success.
  562. return {"allThreadsContinued": True}
  563. @message_handler
  564. def debugpySystemInfo_request(self, request):
  565. result = {"debugpy": {"version": debugpy.__version__}}
  566. if self.server:
  567. try:
  568. pydevd_info = self.server.channel.request("pydevdSystemInfo")
  569. except Exception:
  570. # If the server has already disconnected, or couldn't handle it,
  571. # report what we've got.
  572. pass
  573. else:
  574. result.update(pydevd_info)
  575. return result
  576. @message_handler
  577. def terminate_request(self, request):
  578. # If user specifically requests to terminate, it means that they don't want
  579. # debug session auto-restart kicking in.
  580. self.restart_requested = False
  581. if self._forward_terminate_request:
  582. # According to the spec, terminate should try to do a gracefull shutdown.
  583. # We do this in the server by interrupting the main thread with a Ctrl+C.
  584. # To force the kill a subsequent request would do a disconnect.
  585. #
  586. # We only do this if the onTerminate option is set though (the default
  587. # is a hard-kill for the process and subprocesses).
  588. return self.server.channel.delegate(request)
  589. self.session.finalize('client requested "terminate"', terminate_debuggee=True)
  590. return {}
  591. @message_handler
  592. def disconnect_request(self, request):
  593. # If user specifically requests to disconnect, it means that they don't want
  594. # debug session auto-restart kicking in.
  595. self.restart_requested = False
  596. terminate_debuggee = request("terminateDebuggee", bool, optional=True)
  597. if terminate_debuggee == ():
  598. terminate_debuggee = None
  599. self.session.finalize('client requested "disconnect"', terminate_debuggee)
  600. request.respond({})
  601. if self.using_stdio:
  602. # There's no way for the client to reconnect to this adapter once it disconnects
  603. # from this session, so close any remaining server connections.
  604. servers.stop_serving()
  605. log.info("{0} disconnected from stdio; closing remaining server connections.", self)
  606. for conn in servers.connections():
  607. try:
  608. conn.channel.close()
  609. except Exception:
  610. log.swallow_exception()
  611. # Close the client channel since we disconnected from the client.
  612. try:
  613. self.channel.close()
  614. except Exception:
  615. log.swallow_exception(level="warning")
  616. def disconnect(self):
  617. super().disconnect()
  618. def report_sockets(self):
  619. socks = [
  620. {
  621. "host": host,
  622. "port": port,
  623. "internal": listener is not clients.listener,
  624. }
  625. for listener in [clients.listener, launchers.listener, servers.listener]
  626. if listener is not None
  627. for (host, port) in [sockets.get_address(listener)]
  628. ]
  629. self.channel.send_event(
  630. "debugpySockets",
  631. {
  632. "sockets": socks
  633. },
  634. )
  635. def notify_of_subprocess(self, conn):
  636. log.info("{1} is a subprocess of {0}.", self, conn)
  637. with self.session:
  638. if self.start_request is None or conn in self.known_subprocesses:
  639. return
  640. if "processId" in self.start_request.arguments:
  641. log.warning(
  642. "Not reporting subprocess for {0}, because the parent process "
  643. 'was attached to using "processId" rather than "port".',
  644. self.session,
  645. )
  646. return
  647. log.info("Notifying {0} about {1}.", self, conn)
  648. body = dict(self.start_request.arguments)
  649. self.known_subprocesses.add(conn)
  650. self.session.notify_changed()
  651. for key in "processId", "listen", "preLaunchTask", "postDebugTask", "request", "restart":
  652. body.pop(key, None)
  653. body["name"] = "Subprocess {0}".format(conn.pid)
  654. body["subProcessId"] = conn.pid
  655. for key in "args", "processName", "pythonArgs":
  656. body.pop(key, None)
  657. host = body.pop("host", None)
  658. port = body.pop("port", None)
  659. if "connect" not in body:
  660. body["connect"] = {}
  661. if "host" not in body["connect"]:
  662. localhost = sockets.get_default_localhost()
  663. body["connect"]["host"] = host or localhost
  664. if "port" not in body["connect"]:
  665. if port is None:
  666. _, port = sockets.get_address(listener)
  667. body["connect"]["port"] = port
  668. if self.capabilities["supportsStartDebuggingRequest"]:
  669. self.channel.request("startDebugging", {
  670. "request": "attach",
  671. "configuration": body,
  672. })
  673. else:
  674. body["request"] = "attach"
  675. self.channel.send_event("debugpyAttach", body)
  676. def serve(host, port):
  677. global listener
  678. listener = sockets.serve("Client", Client, host, port)
  679. sessions.report_sockets()
  680. return sockets.get_address(listener)
  681. def stop_serving():
  682. global listener
  683. if listener is not None:
  684. try:
  685. listener.close()
  686. except Exception:
  687. log.swallow_exception(level="warning")
  688. listener = None
  689. sessions.report_sockets()