Format: text/x-python
⬇ Download File (16.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 json
   6  import os
   7  import re
   8  import sys
   9  from importlib.util import find_spec
  10  from typing import Any, Union, Tuple, Dict
  11  
  12  # debugpy.__main__ should have preloaded pydevd properly before importing this module.
  13  # Otherwise, some stdlib modules above might have had imported threading before pydevd
  14  # could perform the necessary detours in it.
  15  assert "pydevd" in sys.modules
  16  import pydevd
  17  
  18  # Note: use the one bundled from pydevd so that it's invisible for the user.
  19  from _pydevd_bundle import pydevd_runpy as runpy
  20  
  21  import debugpy
  22  import debugpy.server
  23  from debugpy.common import log
  24  from debugpy.server import api
  25  
  26  
  27  TARGET = "<filename> | -m <module> | -c <code> | --pid <pid>"
  28  
  29  HELP = """debugpy {0}
  30  See https://aka.ms/debugpy for documentation.
  31  
  32  Usage: debugpy --listen | --connect
  33                 [<host>:]<port>
  34                 [--wait-for-client]
  35                 [--configure-<name> <value>]...
  36                 [--log-to <path>] [--log-to-stderr]
  37                 {1}
  38                 [<arg>]...
  39  """.format(
  40      debugpy.__version__, TARGET
  41  )
  42  
  43  
  44  class Options(object):
  45      mode = None
  46      address: Union[Tuple[str, int], None] = None
  47      log_to = None
  48      log_to_stderr = False
  49      target: Union[str, None] = None
  50      target_kind: Union[str, None] = None
  51      wait_for_client = False
  52      adapter_access_token = None
  53      config: Dict[str, Any] = {}
  54  
  55  
  56  options = Options()
  57  options.config = {"qt": "none", "subProcess": True}
  58  
  59  
  60  def in_range(parser, start, stop):
  61      def parse(s):
  62          n = parser(s)
  63          if start is not None and n < start:
  64              raise ValueError("must be >= {0}".format(start))
  65          if stop is not None and n >= stop:
  66              raise ValueError("must be < {0}".format(stop))
  67          return n
  68  
  69      return parse
  70  
  71  
  72  pid = in_range(int, 0, None)
  73  
  74  
  75  def print_help_and_exit(switch, it):
  76      print(HELP, file=sys.stderr)
  77      sys.exit(0)
  78  
  79  
  80  def print_version_and_exit(switch, it):
  81      print(debugpy.__version__)
  82      sys.exit(0)
  83  
  84  
  85  def set_arg(varname, parser=(lambda x: x)):
  86      def do(arg, it):
  87          value = parser(next(it))
  88          setattr(options, varname, value)
  89  
  90      return do
  91  
  92  
  93  def set_const(varname, value):
  94      def do(arg, it):
  95          setattr(options, varname, value)
  96  
  97      return do
  98  
  99  
 100  def set_address(mode):
 101      def do(arg, it):
 102          if options.address is not None:
 103              raise ValueError("--listen and --connect are mutually exclusive")
 104  
 105          # It's either host:port, or just port.
 106          value = next(it)
 107          host, sep, port = value.partition(":")
 108          if not sep:
 109              host = "127.0.0.1"
 110              port = value
 111          try:
 112              port = int(port)
 113          except Exception:
 114              port = -1
 115          if not (0 <= port < 2**16):
 116              raise ValueError("invalid port number")
 117  
 118          options.mode = mode
 119          options.address = (host, port)
 120  
 121      return do
 122  
 123  
 124  def set_config(arg, it):
 125      prefix = "--configure-"
 126      assert arg.startswith(prefix)
 127      name = arg[len(prefix) :]
 128      value = next(it)
 129  
 130      if name not in options.config:
 131          raise ValueError("unknown property {0!r}".format(name))
 132  
 133      expected_type = type(options.config[name])
 134      try:
 135          if expected_type is bool:
 136              value = {"true": True, "false": False}[value.lower()]
 137          else:
 138              value = expected_type(value)
 139      except Exception:
 140          raise ValueError("{0!r} must be a {1}".format(name, expected_type.__name__))
 141  
 142      options.config[name] = value
 143  
 144  
 145  def set_target(kind: str, parser=(lambda x: x), positional=False):
 146      def do(arg, it):
 147          options.target_kind = kind
 148          target = parser(arg if positional else next(it))
 149  
 150          if isinstance(target, bytes):
 151              # target may be the code, so, try some additional encodings...
 152              try:
 153                  target = target.decode(sys.getfilesystemencoding())
 154              except UnicodeDecodeError:
 155                  try:
 156                      target = target.decode("utf-8")
 157                  except UnicodeDecodeError:
 158                      import locale
 159  
 160                      target = target.decode(locale.getpreferredencoding(False))
 161  
 162          options.target = target
 163  
 164      return do
 165  
 166  
 167  # fmt: off
 168  switches = [
 169      # Switch                    Placeholder         Action
 170      # ======                    ===========         ======
 171  
 172      # Switches that are documented for use by end users.
 173      ("-(\\?|h|-help)",          None,               print_help_and_exit),
 174      ("-(V|-version)",           None,               print_version_and_exit),
 175      ("--log-to" ,               "<path>",           set_arg("log_to")),
 176      ("--log-to-stderr",         None,               set_const("log_to_stderr", True)),
 177      ("--listen",                "<address>",        set_address("listen")),
 178      ("--connect",               "<address>",        set_address("connect")),
 179      ("--wait-for-client",       None,               set_const("wait_for_client", True)),
 180      ("--configure-.+",          "<value>",          set_config),
 181  
 182      # Switches that are used internally by the client or debugpy itself.
 183      ("--adapter-access-token",   "<token>",         set_arg("adapter_access_token")),
 184  
 185      # Targets. The "" entry corresponds to positional command line arguments,
 186      # i.e. the ones not preceded by any switch name.
 187      ("",                        "<filename>",       set_target("file", positional=True)),
 188      ("-m",                      "<module>",         set_target("module")),
 189      ("-c",                      "<code>",           set_target("code")),
 190      ("--pid",                   "<pid>",            set_target("pid", pid)),
 191  ]
 192  # fmt: on
 193  
 194  
 195  # Consume all the args from argv
 196  def consume_argv():
 197      while len(sys.argv) >= 2:
 198          value = sys.argv[1]
 199          del sys.argv[1]
 200          yield value
 201  
 202  
 203  # Consume all the args from a given list
 204  def consume_args(args: list):
 205      if args is sys.argv:
 206          yield from consume_argv()
 207      else:
 208          while args:
 209              value = args[0]
 210              del args[0]
 211              yield value
 212  
 213  
 214  # Parse the args from the command line, then from the environment.
 215  # Args from the environment are only used if they are not already set from the command line.
 216  def parse_args():
 217  
 218      # keep track of the switches we've seen so far
 219      seen = set()
 220  
 221      parse_args_from_command_line(seen)
 222      parse_args_from_environment(seen)
 223  
 224      # if the target is not set, or is empty, this is an error
 225      if options.target is None or options.target == "":
 226          raise ValueError("missing target: " + TARGET)
 227  
 228      if options.mode is None:
 229          raise ValueError("either --listen or --connect is required")
 230      if options.adapter_access_token is not None and options.mode != "connect":
 231          raise ValueError("--adapter-access-token requires --connect")
 232      if options.target_kind == "pid" and options.wait_for_client:
 233          raise ValueError("--pid does not support --wait-for-client")
 234  
 235      assert options.target_kind is not None
 236      assert options.address is not None
 237  
 238  
 239  def parse_args_from_command_line(seen: set):
 240      parse_args_helper(sys.argv, seen)
 241  
 242  
 243  def parse_args_from_environment(seenFromCommandLine: set):
 244      args = os.environ.get("DEBUGPY_EXTRA_ARGV")
 245      if not args:
 246          return
 247  
 248      argsList = args.split()
 249  
 250      seenFromEnvironment = set()
 251      parse_args_helper(argsList, seenFromCommandLine, seenFromEnvironment, True)
 252  
 253  
 254  def parse_args_helper(
 255      args: list,
 256      seenFromCommandLine: set,
 257      seenFromEnvironment: set = set(),
 258      isFromEnvironment=False,
 259  ):
 260      iterator = consume_args(args)
 261  
 262      while True:
 263          try:
 264              arg = next(iterator)
 265          except StopIteration:
 266              break
 267  
 268          switch = arg
 269          if not switch.startswith("-"):
 270              switch = ""
 271          for pattern, placeholder, action in switches:
 272              if re.match("^(" + pattern + ")$", switch):
 273                  break
 274          else:
 275              raise ValueError("unrecognized switch " + switch)
 276  
 277          # if we're parsing from the command line, and we've already seen the switch on the command line, this is an error
 278          if not isFromEnvironment and switch in seenFromCommandLine:
 279              raise ValueError("duplicate switch on command line: " + switch)
 280          # if we're parsing from the environment, and we've already seen the switch in the environment, this is an error
 281          elif isFromEnvironment and switch in seenFromEnvironment:
 282              raise ValueError("duplicate switch from environment: " + switch)
 283          # if we're parsing from the environment, and we've already seen the switch on the command line, skip it, since command line takes precedence
 284          elif isFromEnvironment and switch in seenFromCommandLine:
 285              continue
 286          # otherwise, the switch is new, so add it to the appropriate set
 287          else:
 288              if isFromEnvironment:
 289                  seenFromEnvironment.add(switch)
 290              else:
 291                  seenFromCommandLine.add(switch)
 292  
 293          # process the switch, running the corresponding action
 294          try:
 295              action(arg, iterator)
 296          except StopIteration:
 297              assert placeholder is not None
 298              raise ValueError("{0}: missing {1}".format(switch, placeholder))
 299          except Exception as exc:
 300              raise ValueError("invalid {0} {1}: {2}".format(switch, placeholder, exc))
 301  
 302          # If we're parsing the command line, we're done after we've processed the target
 303          # Otherwise, we need to keep parsing until all args are consumed, since the target may be set from the command line
 304          # already, but there might be additional args in the environment that we want to process.
 305          if not isFromEnvironment and options.target is not None:
 306              break
 307  
 308  
 309  def start_debugging(argv_0):
 310      # We need to set up sys.argv[0] before invoking either listen() or connect(),
 311      # because they use it to report the "process" event. Thus, we can't rely on
 312      # run_path() and run_module() doing that, even though they will eventually.
 313      sys.argv[0] = argv_0
 314  
 315      log.debug("sys.argv after patching: {0!r}", sys.argv)
 316  
 317      debugpy.configure(options.config)
 318  
 319      if os.environ.get("DEBUGPY_RUNNING", "false") != "true":
 320          if options.mode == "listen" and options.address is not None:
 321              debugpy.listen(options.address)
 322          elif options.mode == "connect" and options.address is not None:
 323              debugpy.connect(options.address, access_token=options.adapter_access_token)
 324          else:
 325              raise AssertionError(repr(options.mode))
 326  
 327          if options.wait_for_client:
 328              debugpy.wait_for_client()
 329  
 330      os.environ["DEBUGPY_RUNNING"] = "true"
 331  
 332  
 333  def run_file():
 334      target = options.target
 335      start_debugging(target)
 336  
 337      # run_path has one difference with invoking Python from command-line:
 338      # if the target is a file (rather than a directory), it does not add its
 339      # parent directory to sys.path. Thus, importing other modules from the
 340      # same directory is broken unless sys.path is patched here.
 341  
 342      if target is not None and os.path.isfile(target):
 343          dir = os.path.dirname(target)
 344          sys.path.insert(0, dir)
 345      else:
 346          log.debug("Not a file: {0!r}", target)
 347  
 348      log.describe_environment("Pre-launch environment:")
 349  
 350      log.info("Running file {0!r}", target)
 351      runpy.run_path(target, run_name="__main__")
 352  
 353  
 354  def run_module():
 355      # Add current directory to path, like Python itself does for -m. This must
 356      # be in place before trying to use find_spec below to resolve submodules.
 357      sys.path.insert(0, str(""))
 358  
 359      # We want to do the same thing that run_module() would do here, without
 360      # actually invoking it.
 361      argv_0 = sys.argv[0]
 362      try:
 363          spec = None if options.target is None else find_spec(options.target)
 364          if spec is not None:
 365              argv_0 = spec.origin
 366      except Exception:
 367          log.swallow_exception("Error determining module path for sys.argv")
 368  
 369      start_debugging(argv_0)
 370      log.describe_environment("Pre-launch environment:")
 371      log.info("Running module {0!r}", options.target)
 372  
 373      # Docs say that runpy.run_module is equivalent to -m, but it's not actually
 374      # the case for packages - -m sets __name__ to "__main__", but run_module sets
 375      # it to "pkg.__main__". This breaks everything that uses the standard pattern
 376      # __name__ == "__main__" to detect being run as a CLI app. On the other hand,
 377      # runpy._run_module_as_main is a private function that actually implements -m.
 378      try:
 379          run_module_as_main = runpy._run_module_as_main
 380      except AttributeError:
 381          log.warning("runpy._run_module_as_main is missing, falling back to run_module.")
 382          runpy.run_module(options.target, alter_sys=True)
 383      else:
 384          run_module_as_main(options.target, alter_argv=True)
 385  
 386  
 387  def run_code():
 388      if options.target is not None:
 389          # Add current directory to path, like Python itself does for -c.
 390          sys.path.insert(0, str(""))
 391          code = compile(options.target, str("<string>"), str("exec"))
 392  
 393          start_debugging(str("-c"))
 394  
 395          log.describe_environment("Pre-launch environment:")
 396          log.info("Running code:\n\n{0}", options.target)
 397  
 398          eval(code, {})
 399      else:
 400          log.error("No target to run.")
 401  
 402  
 403  def attach_to_pid():
 404      pid = options.target
 405      log.info("Attaching to process with PID={0}", pid)
 406  
 407      encode = lambda s: list(bytearray(s.encode("utf-8"))) if s is not None else None
 408  
 409      script_dir = os.path.dirname(debugpy.server.__file__)
 410      assert os.path.exists(script_dir)
 411      script_dir = encode(script_dir)
 412  
 413      setup = {
 414          "mode": options.mode,
 415          "address": options.address,
 416          "wait_for_client": options.wait_for_client,
 417          "log_to": options.log_to,
 418          "adapter_access_token": options.adapter_access_token,
 419      }
 420      setup = encode(json.dumps(setup))
 421  
 422      python_code = """
 423  import codecs;
 424  import json;
 425  import sys;
 426  
 427  decode = lambda s: codecs.utf_8_decode(bytearray(s))[0] if s is not None else None;
 428  
 429  script_dir = decode({script_dir});
 430  setup = json.loads(decode({setup}));
 431  
 432  sys.path.insert(0, script_dir);
 433  import attach_pid_injected;
 434  del sys.path[0];
 435  
 436  attach_pid_injected.attach(setup);
 437  """
 438      python_code = (
 439          python_code.replace("\r", "")
 440          .replace("\n", "")
 441          .format(script_dir=script_dir, setup=setup)
 442      )
 443      log.info("Code to be injected: \n{0}", python_code.replace(";", ";\n"))
 444  
 445      # pydevd restriction on characters in injected code.
 446      assert not (
 447          {'"', "'", "\r", "\n"} & set(python_code)
 448      ), "Injected code should not contain any single quotes, double quotes, or newlines."
 449  
 450      pydevd_attach_to_process_path = os.path.join(
 451          os.path.dirname(pydevd.__file__), "pydevd_attach_to_process"
 452      )
 453  
 454      assert os.path.exists(pydevd_attach_to_process_path)
 455      sys.path.append(pydevd_attach_to_process_path)
 456  
 457      try:
 458          import add_code_to_python_process  # noqa
 459  
 460          log.info("Injecting code into process with PID={0} ...", pid)
 461          add_code_to_python_process.run_python_code(
 462              pid,
 463              python_code,
 464              connect_debugger_tracing=True,
 465              show_debug_info=int(os.getenv("DEBUGPY_ATTACH_BY_PID_DEBUG_INFO", "0")),
 466          )
 467      except Exception:
 468          log.reraise_exception("Code injection into PID={0} failed:", pid)
 469      log.info("Code injection into PID={0} completed.", pid)
 470  
 471  
 472  def main():
 473      original_argv = list(sys.argv)
 474      try:
 475          parse_args()
 476      except Exception as exc:
 477          print(str(HELP) + str("\nError: ") + str(exc), file=sys.stderr)
 478          sys.exit(2)
 479  
 480      if options.log_to is not None:
 481          debugpy.log_to(options.log_to)
 482      if options.log_to_stderr:
 483          debugpy.log_to(sys.stderr)
 484  
 485      api.ensure_logging()
 486  
 487      log.info(
 488          str("sys.argv before parsing: {0!r}\n" "         after parsing:  {1!r}"),
 489          original_argv,
 490          sys.argv,
 491      )
 492  
 493      try:
 494          if options.target_kind is not None:
 495              run = {
 496                  "file": run_file,
 497                  "module": run_module,
 498                  "code": run_code,
 499                  "pid": attach_to_pid,
 500              }[options.target_kind]
 501              run()
 502      except SystemExit as exc:
 503          log.reraise_exception(
 504              "Debuggee exited via SystemExit: {0!r}", exc.code, level="debug"
 505          )
 506