launchers.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  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. import os
  5. import subprocess
  6. import sys
  7. from debugpy import adapter, common
  8. from debugpy.common import log, messaging, sockets
  9. from debugpy.adapter import components, servers, sessions
  10. listener = None
  11. class Launcher(components.Component):
  12. """Handles the launcher side of a debug session."""
  13. message_handler = components.Component.message_handler
  14. def __init__(self, session, stream):
  15. with session:
  16. assert not session.launcher
  17. super().__init__(session, stream)
  18. self.pid = None
  19. """Process ID of the debuggee process, as reported by the launcher."""
  20. self.exit_code = None
  21. """Exit code of the debuggee process."""
  22. session.launcher = self
  23. @message_handler
  24. def process_event(self, event):
  25. self.pid = event("systemProcessId", int)
  26. self.client.propagate_after_start(event)
  27. @message_handler
  28. def output_event(self, event):
  29. self.client.propagate_after_start(event)
  30. @message_handler
  31. def exited_event(self, event):
  32. self.exit_code = event("exitCode", int)
  33. # We don't want to tell the client about this just yet, because it will then
  34. # want to disconnect, and the launcher might still be waiting for keypress
  35. # (if wait-on-exit was enabled). Instead, we'll report the event when we
  36. # receive "terminated" from the launcher, right before it exits.
  37. @message_handler
  38. def terminated_event(self, event):
  39. try:
  40. self.client.channel.send_event("exited", {"exitCode": self.exit_code})
  41. except Exception:
  42. pass
  43. self.channel.close()
  44. def terminate_debuggee(self):
  45. with self.session:
  46. if self.exit_code is None:
  47. try:
  48. self.channel.request("terminate")
  49. except Exception:
  50. pass
  51. def spawn_debuggee(
  52. session,
  53. start_request,
  54. python,
  55. launcher_path,
  56. adapter_host,
  57. args,
  58. shell_expand_args,
  59. cwd,
  60. console,
  61. console_title,
  62. sudo,
  63. ):
  64. global listener
  65. # -E tells sudo to propagate environment variables to the target process - this
  66. # is necessary for launcher to get DEBUGPY_LAUNCHER_PORT and DEBUGPY_LOG_DIR.
  67. cmdline = ["sudo", "-E"] if sudo else []
  68. cmdline += python
  69. cmdline += [launcher_path]
  70. env = {}
  71. arguments = dict(start_request.arguments)
  72. if not session.no_debug:
  73. _, arguments["port"] = sockets.get_address(servers.listener)
  74. arguments["adapterAccessToken"] = adapter.access_token
  75. def on_launcher_connected(sock):
  76. listener.close()
  77. stream = messaging.JsonIOStream.from_socket(sock)
  78. Launcher(session, stream)
  79. try:
  80. listener = sockets.serve(
  81. "Launcher", on_launcher_connected, adapter_host, backlog=1
  82. )
  83. except Exception as exc:
  84. raise start_request.cant_handle(
  85. "{0} couldn't create listener socket for launcher: {1}", session, exc
  86. )
  87. sessions.report_sockets()
  88. try:
  89. launcher_host, launcher_port = sockets.get_address(listener)
  90. localhost = sockets.get_default_localhost()
  91. launcher_addr = (
  92. launcher_port
  93. if launcher_host == localhost
  94. else f"{launcher_host}:{launcher_port}"
  95. )
  96. cmdline += [str(launcher_addr), "--"]
  97. cmdline += args
  98. if log.log_dir is not None:
  99. env[str("DEBUGPY_LOG_DIR")] = log.log_dir
  100. if log.stderr.levels != {"warning", "error"}:
  101. env[str("DEBUGPY_LOG_STDERR")] = str(" ".join(log.stderr.levels))
  102. if console == "internalConsole":
  103. log.info("{0} spawning launcher: {1!r}", session, cmdline)
  104. try:
  105. # If we are talking to the client over stdio, sys.stdin and sys.stdout
  106. # are redirected to avoid mangling the DAP message stream. Make sure
  107. # the launcher also respects that.
  108. subprocess.Popen(
  109. cmdline,
  110. cwd=cwd,
  111. env=dict(list(os.environ.items()) + list(env.items())),
  112. stdin=sys.stdin,
  113. stdout=sys.stdout,
  114. stderr=sys.stderr,
  115. )
  116. except Exception as exc:
  117. raise start_request.cant_handle("Failed to spawn launcher: {0}", exc)
  118. else:
  119. log.info('{0} spawning launcher via "runInTerminal" request.', session)
  120. session.client.capabilities.require("supportsRunInTerminalRequest")
  121. kinds = {"integratedTerminal": "integrated", "externalTerminal": "external"}
  122. request_args = {
  123. "kind": kinds[console],
  124. "title": console_title,
  125. "args": cmdline,
  126. "env": env,
  127. }
  128. if cwd is not None:
  129. request_args["cwd"] = cwd
  130. if shell_expand_args:
  131. request_args["argsCanBeInterpretedByShell"] = True
  132. # VS Code debugger extension may pass us an argument indicating the
  133. # quoting character to use in the terminal. Otherwise default based on platform.
  134. default_quote = '"' if os.name != "nt" else "'"
  135. quote_char = arguments["terminalQuoteCharacter"] if "terminalQuoteCharacter" in arguments else default_quote
  136. # VS code doesn't quote arguments if `argsCanBeInterpretedByShell` is true,
  137. # so we need to do it ourselves for the arguments up to the first argument passed to
  138. # debugpy (this should be the python file to run).
  139. args = request_args["args"]
  140. for i in range(len(args)):
  141. s = args[i]
  142. if " " in s and not ((s.startswith('"') and s.endswith('"')) or (s.startswith("'") and s.endswith("'"))):
  143. s = f"{quote_char}{s}{quote_char}"
  144. args[i] = s
  145. if i > 0 and args[i-1] == "--":
  146. break
  147. try:
  148. # It is unspecified whether this request receives a response immediately, or only
  149. # after the spawned command has completed running, so do not block waiting for it.
  150. session.client.channel.send_request("runInTerminal", request_args)
  151. except messaging.MessageHandlingError as exc:
  152. exc.propagate(start_request)
  153. # If using sudo, it might prompt for password, and launcher won't start running
  154. # until the user enters it, so don't apply timeout in that case.
  155. if not session.wait_for(
  156. lambda: session.launcher,
  157. timeout=(None if sudo else common.PROCESS_SPAWN_TIMEOUT),
  158. ):
  159. raise start_request.cant_handle("Timed out waiting for launcher to connect")
  160. try:
  161. session.launcher.channel.request(start_request.command, arguments)
  162. except messaging.MessageHandlingError as exc:
  163. exc.propagate(start_request)
  164. if not session.wait_for(
  165. lambda: session.launcher.pid is not None,
  166. timeout=common.PROCESS_SPAWN_TIMEOUT,
  167. ):
  168. raise start_request.cant_handle(
  169. 'Timed out waiting for "process" event from launcher'
  170. )
  171. if session.no_debug:
  172. return
  173. # Wait for the first incoming connection regardless of the PID - it won't
  174. # necessarily match due to the use of stubs like py.exe or "conda run".
  175. conn = servers.wait_for_connection(
  176. session, lambda conn: True, timeout=common.PROCESS_SPAWN_TIMEOUT
  177. )
  178. if conn is None:
  179. raise start_request.cant_handle("Timed out waiting for debuggee to spawn")
  180. conn.attach_to_session(session)
  181. finally:
  182. listener.close()
  183. listener = None
  184. sessions.report_sockets()