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