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