Format: text/x-python
⬇ Download File (8.5 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 argparse
   6  import atexit
   7  import codecs
   8  import locale
   9  import os
  10  import sys
  11  
  12  # WARNING: debugpy and submodules must not be imported on top level in this module,
  13  # and should be imported locally inside main() instead.
  14  
  15  
  16  def main():
  17      args = _parse_argv(sys.argv)
  18  
  19      # If we're talking DAP over stdio, stderr is not guaranteed to be read from,
  20      # so disable it to avoid the pipe filling and locking up. This must be done
  21      # as early as possible, before the logging module starts writing to it.
  22      if args.port is None:
  23          sys.stderr = stderr = open(os.devnull, "w")
  24          atexit.register(stderr.close)
  25  
  26      from debugpy import adapter
  27      from debugpy.common import json, log, sockets
  28      from debugpy.adapter import clients, servers, sessions
  29  
  30      if args.for_server is not None:
  31          if os.name == "posix":
  32              # On POSIX, we need to leave the process group and its session, and then
  33              # daemonize properly by double-forking (first fork already happened when
  34              # this process was spawned).
  35              # NOTE: if process is already the session leader, then
  36              # setsid would fail with `operation not permitted`
  37              if os.getsid(os.getpid()) != os.getpid():
  38                  os.setsid()
  39              if os.fork() != 0:
  40                  sys.exit(0)
  41  
  42          for stdio in sys.stdin, sys.stdout, sys.stderr:
  43              if stdio is not None:
  44                  stdio.close()
  45  
  46      if args.log_stderr:
  47          log.stderr.levels |= set(log.LEVELS)
  48      if args.log_dir is not None:
  49          log.log_dir = args.log_dir
  50  
  51      log.to_file(prefix="debugpy.adapter")
  52      log.describe_environment("debugpy.adapter startup environment:")
  53  
  54      servers.access_token = args.server_access_token
  55      if args.for_server is None:
  56          adapter.access_token = codecs.encode(os.urandom(32), "hex").decode("ascii")
  57  
  58      endpoints = {}
  59      try:
  60          client_host, client_port = clients.serve(args.host, args.port)
  61      except Exception as exc:
  62          if args.for_server is None:
  63              raise
  64          endpoints = {"error": "Can't listen for client connections: " + str(exc)}
  65      else:
  66          endpoints["client"] = {"host": client_host, "port": client_port}
  67  
  68      if args.for_server is not None:
  69          try:
  70              server_host, server_port = servers.serve()
  71          except Exception as exc:
  72              endpoints = {"error": "Can't listen for server connections: " + str(exc)}
  73          else:
  74              endpoints["server"] = {"host": server_host, "port": server_port}
  75  
  76          log.info(
  77              "Sending endpoints info to debug server at localhost:{0}:\n{1}",
  78              args.for_server,
  79              json.repr(endpoints),
  80          )
  81  
  82          try:
  83              sock = sockets.create_client()
  84              try:
  85                  sock.settimeout(None)
  86                  sock.connect(("127.0.0.1", args.for_server))
  87                  sock_io = sock.makefile("wb", 0)
  88                  try:
  89                      sock_io.write(json.dumps(endpoints).encode("utf-8"))
  90                  finally:
  91                      sock_io.close()
  92              finally:
  93                  sockets.close_socket(sock)
  94          except Exception:
  95              log.reraise_exception("Error sending endpoints info to debug server:")
  96  
  97          if "error" in endpoints:
  98              log.error("Couldn't set up endpoints; exiting.")
  99              sys.exit(1)
 100  
 101      listener_file = os.getenv("DEBUGPY_ADAPTER_ENDPOINTS")
 102      if listener_file is not None:
 103          log.info(
 104              "Writing endpoints info to {0!r}:\n{1}", listener_file, json.repr(endpoints)
 105          )
 106  
 107          def delete_listener_file():
 108              log.info("Listener ports closed; deleting {0!r}", listener_file)
 109              try:
 110                  os.remove(listener_file)
 111              except Exception:
 112                  log.swallow_exception(
 113                      "Failed to delete {0!r}", listener_file, level="warning"
 114                  )
 115  
 116          try:
 117              with open(listener_file, "w") as f:
 118                  atexit.register(delete_listener_file)
 119                  print(json.dumps(endpoints), file=f)
 120          except Exception:
 121              log.reraise_exception("Error writing endpoints info to file:")
 122  
 123      if args.port is None:
 124          clients.Client("stdio")
 125  
 126      # These must be registered after the one above, to ensure that the listener sockets
 127      # are closed before the endpoint info file is deleted - this way, another process
 128      # can wait for the file to go away as a signal that the ports are no longer in use.
 129      atexit.register(servers.stop_serving)
 130      atexit.register(clients.stop_serving)
 131  
 132      servers.wait_until_disconnected()
 133      log.info("All debug servers disconnected; waiting for remaining sessions...")
 134  
 135      sessions.wait_until_ended()
 136      log.info("All debug sessions have ended; exiting.")
 137  
 138  
 139  def _parse_argv(argv):
 140      parser = argparse.ArgumentParser()
 141  
 142      parser.add_argument(
 143          "--for-server", type=int, metavar="PORT", help=argparse.SUPPRESS
 144      )
 145  
 146      parser.add_argument(
 147          "--port",
 148          type=int,
 149          default=None,
 150          metavar="PORT",
 151          help="start the adapter in debugServer mode on the specified port",
 152      )
 153  
 154      parser.add_argument(
 155          "--host",
 156          type=str,
 157          default="127.0.0.1",
 158          metavar="HOST",
 159          help="start the adapter in debugServer mode on the specified host",
 160      )
 161  
 162      parser.add_argument(
 163          "--access-token", type=str, help="access token expected from the server"
 164      )
 165  
 166      parser.add_argument(
 167          "--server-access-token", type=str, help="access token expected by the server"
 168      )
 169  
 170      parser.add_argument(
 171          "--log-dir",
 172          type=str,
 173          metavar="DIR",
 174          help="enable logging and use DIR to save adapter logs",
 175      )
 176  
 177      parser.add_argument(
 178          "--log-stderr", action="store_true", help="enable logging to stderr"
 179      )
 180  
 181      args = parser.parse_args(argv[1:])
 182  
 183      if args.port is None:
 184          if args.log_stderr:
 185              parser.error("--log-stderr requires --port")
 186          if args.for_server is not None:
 187              parser.error("--for-server requires --port")
 188  
 189      return args
 190  
 191  
 192  if __name__ == "__main__":
 193      # debugpy can also be invoked directly rather than via -m. In this case, the first
 194      # entry on sys.path is the one added automatically by Python for the directory
 195      # containing this file. This means that import debugpy will not work, since we need
 196      # the parent directory of debugpy/ to be in sys.path, rather than debugpy/adapter/.
 197      #
 198      # The other issue is that many other absolute imports will break, because they
 199      # will be resolved relative to debugpy/adapter/ - e.g. `import state` will then try
 200      # to import debugpy/adapter/state.py.
 201      #
 202      # To fix both, we need to replace the automatically added entry such that it points
 203      # at parent directory of debugpy/ instead of debugpy/adapter, import debugpy with that
 204      # in sys.path, and then remove the first entry entry altogether, so that it doesn't
 205      # affect any further imports we might do. For example, suppose the user did:
 206      #
 207      #   python /foo/bar/debugpy/adapter ...
 208      #
 209      # At the beginning of this script, sys.path will contain "/foo/bar/debugpy/adapter"
 210      # as the first entry. What we want is to replace it with "/foo/bar', then import
 211      # debugpy with that in effect, and then remove the replaced entry before any more
 212      # code runs. The imported debugpy module will remain in sys.modules, and thus all
 213      # future imports of it or its submodules will resolve accordingly.
 214      if "debugpy" not in sys.modules:
 215          # Do not use dirname() to walk up - this can be a relative path, e.g. ".".
 216          if os.name == "nt":
 217              import pathlib
 218  
 219              windows_path = pathlib.Path(sys.path[0])
 220              sys.path[0] = str(windows_path.parent.parent)
 221          else:
 222              sys.path[0] = sys.path[0] + "/../../"
 223          __import__("debugpy")
 224          del sys.path[0]
 225  
 226      # Apply OS-global and user-specific locale settings.
 227      try:
 228          locale.setlocale(locale.LC_ALL, "")
 229      except Exception:
 230          # On POSIX, locale is set via environment variables, and this can fail if
 231          # those variables reference a non-existing locale. Ignore and continue using
 232          # the default "C" locale if so.
 233          pass
 234  
 235      main()
 236