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