cli.py 18 KB

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