Example #1
0
def test_unicode_dir(tmpdir, run, target):
    from debugpy.common import compat

    unicode_chars = "รก"

    directory = os.path.join(compat.force_unicode(str(tmpdir), "ascii"),
                             unicode_chars)
    directory = compat.filename_str(directory)
    os.makedirs(directory)

    code_to_debug = os.path.join(directory,
                                 compat.filename_str("experiment.py"))
    with open(code_to_debug, "wb") as stream:
        stream.write(b"""
import debuggee
from debuggee import backchannel

debuggee.setup()
backchannel.send('ok')
""")

    with debug.Session() as session:
        backchannel = session.open_backchannel()
        with run(session, target(compat.filename_str(code_to_debug))):
            pass

        received = backchannel.receive()
        assert received == "ok"
Example #2
0
 def for_popen(self):
     """Returns a copy of this dict, with all strings converted to the type
     suitable for subprocess.Popen() and other similar APIs.
     """
     return {
         compat.filename_str(k): compat.filename_str(v)
         for k, v in self.items()
     }
Example #3
0
    def spawn_debuggee(self, args, cwd=None, exe=sys.executable, setup=None):
        assert self.debuggee is None
        assert not len(self.captured_output - {"stdout", "stderr"})

        args = [exe] + [
            compat.filename_str(s.strpath if isinstance(s, py.path.local) else s)
            for s in args
        ]

        cwd = compat.filename_str(cwd) if isinstance(cwd, py.path.local) else cwd

        env = self._make_env(self.spawn_debuggee.env, codecov=False)
        env["DEBUGPY_ADAPTER_ENDPOINTS"] = self.adapter_endpoints = (
            self.tmpdir / "adapter_endpoints"
        )
        if setup is not None:
            env["DEBUGPY_TEST_DEBUGGEE_SETUP"] = setup

        log.info(
            "Spawning {0}:\n\n"
            "Current directory: {1!j}\n\n"
            "Command line: {2!j}\n\n"
            "Environment variables: {3!j}\n\n",
            self.debuggee_id,
            cwd,
            args,
            env,
        )

        popen_fds = {}
        capture_fds = {}
        for stream_name in self.captured_output:
            rfd, wfd = os.pipe()
            popen_fds[stream_name] = wfd
            capture_fds[stream_name] = rfd
        self.debuggee = psutil.Popen(
            args,
            cwd=cwd,
            env=env.for_popen(),
            bufsize=0,
            stdin=subprocess.PIPE,
            **popen_fds
        )
        log.info("Spawned {0} with PID={1}", self.debuggee_id, self.debuggee.pid)
        watchdog.register_spawn(self.debuggee.pid, self.debuggee_id)

        if len(capture_fds):
            self.captured_output = output.CapturedOutput(self, **capture_fds)
        for fd in popen_fds.values():
            os.close(fd)
Example #4
0
def attach_connect(session, target, method, cwd=None, wait=True, log_dir=None):
    log.info("Attaching {0} to {1} by socket using {2}.", session, target,
             method.upper())

    assert method in ("api", "cli")

    config = _attach_common_config(session, target, cwd)
    config["connect"] = {}
    config["connect"]["host"] = host = attach_connect.host
    config["connect"]["port"] = port = attach_connect.port

    if method == "cli":
        args = [
            os.path.dirname(debugpy.__file__),
            "--listen",
            compat.filename_str(host) + ":" + str(port),
        ]
        if wait:
            args += ["--wait-for-client"]
        if log_dir is not None:
            args += ["--log-to", log_dir]
        if "subProcess" in config:
            args += ["--configure-subProcess", str(config["subProcess"])]
        debuggee_setup = None
    elif method == "api":
        args = []
        api_config = {k: v for k, v in config.items() if k in {"subProcess"}}
        debuggee_setup = """
import debugpy
if {log_dir!r}:
    debugpy.log_to({log_dir!r})
debugpy.configure({api_config!r})
debugpy.listen(({host!r}, {port!r}))
if {wait!r}:
    debugpy.wait_for_client()
"""
        debuggee_setup = fmt(
            debuggee_setup,
            host=host,
            port=port,
            wait=wait,
            log_dir=log_dir,
            api_config=api_config,
        )
    else:
        raise ValueError
    args += target.cli(session.spawn_debuggee.env)

    try:
        del config["subProcess"]
    except KeyError:
        pass

    session.spawn_debuggee(args, cwd=cwd, setup=debuggee_setup)
    if wait:
        session.wait_for_adapter_socket()

    session.connect_to_adapter((host, port))
    return session.request_attach()
Example #5
0
def attach_listen(session, target, method, cwd=None, log_dir=None):
    log.info("Attaching {0} to {1} by socket using {2}.", session, target,
             method.upper())

    assert method in ("api", "cli")

    config = _attach_common_config(session, target, cwd)
    config["listen"] = {}
    config["listen"]["host"] = host = attach_listen.host
    config["listen"]["port"] = port = attach_listen.port

    if method == "cli":
        args = [
            os.path.dirname(debugpy.__file__),
            "--connect",
            compat.filename_str(host) + ":" + str(port),
        ]
        if log_dir is not None:
            args += ["--log-to", log_dir]
        if "subProcess" in config:
            args += ["--configure-subProcess", str(config["subProcess"])]
        debuggee_setup = None
    elif method == "api":
        args = []
        api_config = {k: v for k, v in config.items() if k in {"subProcess"}}
        debuggee_setup = """
import debugpy
if {log_dir!r}:
    debugpy.log_to({log_dir!r})
debugpy.configure({api_config!r})
debugpy.connect({address!r})
"""
        debuggee_setup = fmt(debuggee_setup,
                             address=(host, port),
                             log_dir=log_dir,
                             api_config=api_config)
    else:
        raise ValueError
    args += target.cli(session.spawn_debuggee.env)

    try:
        del config["subProcess"]
    except KeyError:
        pass

    def spawn_debuggee(occ):
        assert occ.body == some.dict.containing({"host": host, "port": port})
        session.spawn_debuggee(args, cwd=cwd, setup=debuggee_setup)

    session.timeline.when(timeline.Event("debugpyWaitingForServer"),
                          spawn_debuggee)
    session.spawn_adapter(
        args=[] if log_dir is None else ["--log-dir", log_dir])
    return session.request_attach()
Example #6
0
def run_module():
    # Add current directory to path, like Python itself does for -m. This must
    # be in place before trying to use find_spec below to resolve submodules.
    sys.path.insert(0, str(""))

    # We want to do the same thing that run_module() would do here, without
    # actually invoking it. On Python 3, it's exposed as a public API, but
    # on Python 2, we have to invoke a private function in runpy for this.
    # Either way, if it fails to resolve for any reason, just leave argv as is.
    argv_0 = sys.argv[0]
    target_as_str = compat.filename_str(options.target)
    try:
        if sys.version_info >= (3, ):
            from importlib.util import find_spec

            spec = find_spec(target_as_str)
            if spec is not None:
                argv_0 = spec.origin
        else:
            _, _, _, argv_0 = runpy._get_module_details(target_as_str)
    except Exception:
        log.swallow_exception("Error determining module path for sys.argv")

    start_debugging(argv_0)

    # On Python 2, module name must be a non-Unicode string, because it ends up
    # a part of module's __package__, and Python will refuse to run the module
    # if __package__ is Unicode.

    log.describe_environment("Pre-launch environment:")
    log.info("Running module {0!r}", options.target)

    # Docs say that runpy.run_module is equivalent to -m, but it's not actually
    # the case for packages - -m sets __name__ to "__main__", but run_module sets
    # it to "pkg.__main__". This breaks everything that uses the standard pattern
    # __name__ == "__main__" to detect being run as a CLI app. On the other hand,
    # runpy._run_module_as_main is a private function that actually implements -m.
    try:
        run_module_as_main = runpy._run_module_as_main
    except AttributeError:
        log.warning(
            "runpy._run_module_as_main is missing, falling back to run_module."
        )
        runpy.run_module(target_as_str, alter_sys=True)
    else:
        run_module_as_main(target_as_str, alter_argv=True)
Example #7
0
def start_debugging(argv_0):
    # We need to set up sys.argv[0] before invoking either listen() or connect(),
    # because they use it to report the "process" event. Thus, we can't rely on
    # run_path() and run_module() doing that, even though they will eventually.
    sys.argv[0] = compat.filename_str(argv_0)
    log.debug("sys.argv after patching: {0!r}", sys.argv)

    debugpy.configure(options.config)

    if options.mode == "listen":
        debugpy.listen(options.address)
    elif options.mode == "connect":
        debugpy.connect(options.address, access_token=options.adapter_access_token)
    else:
        raise AssertionError(repr(options.mode))

    if options.wait_for_client:
        debugpy.wait_for_client()
Example #8
0
def run_file():
    target = options.target
    start_debugging(target)

    target_as_str = compat.filename_str(target)

    # run_path has one difference with invoking Python from command-line:
    # if the target is a file (rather than a directory), it does not add its
    # parent directory to sys.path. Thus, importing other modules from the
    # same directory is broken unless sys.path is patched here.

    if os.path.isfile(target_as_str):
        dir = os.path.dirname(target_as_str)
        sys.path.insert(0, dir)
    else:
        log.debug("Not a file: {0!r}", target)

    log.describe_environment("Pre-launch environment:")

    log.info("Running file {0!r}", target)
    runpy.run_path(target_as_str, run_name=compat.force_str("__main__"))
Example #9
0
def describe_environment(header):
    import sysconfig
    import site  # noqa

    result = [header, "\n\n"]

    def report(*args, **kwargs):
        result.append(fmt(*args, **kwargs))

    def report_paths(get_paths, label=None):
        prefix = fmt("    {0}: ", label or get_paths)

        expr = None
        if not callable(get_paths):
            expr = get_paths
            get_paths = lambda: util.evaluate(expr)
        try:
            paths = get_paths()
        except AttributeError:
            report("{0}<missing>\n", prefix)
            return
        except Exception:
            swallow_exception(
                "Error evaluating {0}",
                repr(expr) if expr else compat.srcnameof(get_paths),
            )
            return

        if not isinstance(paths, (list, tuple)):
            paths = [paths]

        for p in sorted(paths):
            report("{0}{1}", prefix, p)
            rp = os.path.realpath(p)
            if p != rp:
                report("({0})", rp)
            report("\n")

            prefix = " " * len(prefix)

    report("System paths:\n")
    report_paths("sys.prefix")
    report_paths("sys.base_prefix")
    report_paths("sys.real_prefix")
    report_paths("site.getsitepackages()")
    report_paths("site.getusersitepackages()")

    site_packages = [
        p for p in sys.path if os.path.exists(p)
        and os.path.basename(p) == compat.filename_str("site-packages")
    ]
    report_paths(lambda: site_packages, "sys.path (site-packages)")

    for name in sysconfig.get_path_names():
        expr = fmt("sysconfig.get_path({0!r})", name)
        report_paths(expr)

    report_paths("os.__file__")
    report_paths("threading.__file__")

    result = "".join(result).rstrip("\n")
    info("{0}", result)
Example #10
0
def spawn_debuggee(
    session,
    start_request,
    python,
    launcher_path,
    adapter_host,
    args,
    cwd,
    console,
    console_title,
    sudo,
):
    # -E tells sudo to propagate environment variables to the target process - this
    # is necessary for launcher to get DEBUGPY_LAUNCHER_PORT and DEBUGPY_LOG_DIR.
    cmdline = ["sudo", "-E"] if sudo else []
    cmdline += python
    cmdline += [launcher_path]
    env = {}

    arguments = dict(start_request.arguments)
    if not session.no_debug:
        _, arguments["port"] = servers.listener.getsockname()
        arguments["adapterAccessToken"] = adapter.access_token

    def on_launcher_connected(sock):
        listener.close()
        stream = messaging.JsonIOStream.from_socket(sock)
        Launcher(session, stream)

    try:
        listener = sockets.serve("Launcher",
                                 on_launcher_connected,
                                 adapter_host,
                                 backlog=1)
    except Exception as exc:
        raise start_request.cant_handle(
            "{0} couldn't create listener socket for launcher: {1}", session,
            exc)

    try:
        launcher_host, launcher_port = listener.getsockname()
        launcher_addr = (launcher_port if launcher_host == "127.0.0.1" else
                         fmt("{0}:{1}", launcher_host, launcher_port))
        cmdline += [str(launcher_addr), "--"]
        cmdline += args

        if log.log_dir is not None:
            env[str("DEBUGPY_LOG_DIR")] = compat.filename_str(log.log_dir)
        if log.stderr.levels != {"warning", "error"}:
            env[str("DEBUGPY_LOG_STDERR")] = str(" ".join(log.stderr.levels))

        if console == "internalConsole":
            log.info("{0} spawning launcher: {1!r}", session, cmdline)
            try:
                for i, arg in enumerate(cmdline):
                    try:
                        cmdline[i] = compat.filename_str(arg)
                    except UnicodeEncodeError as exc:
                        raise start_request.cant_handle(
                            "Invalid command line argument {0!j}: {1}", arg,
                            exc)

                # If we are talking to the client over stdio, sys.stdin and sys.stdout
                # are redirected to avoid mangling the DAP message stream. Make sure
                # the launcher also respects that.
                subprocess.Popen(
                    cmdline,
                    cwd=cwd,
                    env=dict(list(os.environ.items()) + list(env.items())),
                    stdin=sys.stdin,
                    stdout=sys.stdout,
                    stderr=sys.stderr,
                )
            except Exception as exc:
                raise start_request.cant_handle(
                    "Failed to spawn launcher: {0}", exc)
        else:
            log.info('{0} spawning launcher via "runInTerminal" request.',
                     session)
            session.client.capabilities.require("supportsRunInTerminalRequest")
            kinds = {
                "integratedTerminal": "integrated",
                "externalTerminal": "external"
            }
            request_args = {
                "kind": kinds[console],
                "title": console_title,
                "args": cmdline,
                "env": env,
            }
            if cwd is not None:
                request_args["cwd"] = cwd
            try:
                session.client.channel.request("runInTerminal", request_args)
            except messaging.MessageHandlingError as exc:
                exc.propagate(start_request)

        # If using sudo, it might prompt for password, and launcher won't start running
        # until the user enters it, so don't apply timeout in that case.
        if not session.wait_for(
                lambda: session.launcher,
                timeout=(None if sudo else common.PROCESS_SPAWN_TIMEOUT),
        ):
            raise start_request.cant_handle(
                "Timed out waiting for launcher to connect")

        try:
            session.launcher.channel.request(start_request.command, arguments)
        except messaging.MessageHandlingError as exc:
            exc.propagate(start_request)

        if session.no_debug:
            return

        if not session.wait_for(
                lambda: session.launcher.pid is not None,
                timeout=common.PROCESS_SPAWN_TIMEOUT,
        ):
            raise start_request.cant_handle(
                'Timed out waiting for "process" event from launcher')

        # Wait for the first incoming connection regardless of the PID - it won't
        # necessarily match due to the use of stubs like py.exe or "conda run".
        conn = servers.wait_for_connection(
            session, lambda conn: True, timeout=common.PROCESS_SPAWN_TIMEOUT)
        if conn is None:
            raise start_request.cant_handle(
                "Timed out waiting for debuggee to spawn")
        conn.attach_to_session(session)

    finally:
        listener.close()
Example #11
0
def spawn_debuggee(session, start_request, args, console, console_title):
    cmdline = [sys.executable, os.path.dirname(launcher.__file__)] + args
    env = {}

    arguments = dict(start_request.arguments)
    if not session.no_debug:
        _, arguments["port"] = servers.listener.getsockname()
        arguments["adapterAccessToken"] = adapter.access_token

    def on_launcher_connected(sock):
        listener.close()
        stream = messaging.JsonIOStream.from_socket(sock)
        Launcher(session, stream)

    try:
        listener = sockets.serve(
            "Launcher", on_launcher_connected, "127.0.0.1", backlog=0
        )
    except Exception as exc:
        raise start_request.cant_handle(
            "{0} couldn't create listener socket for {1}: {2}",
            session,
            session.launcher,
            exc,
        )

    try:
        _, launcher_port = listener.getsockname()

        env[str("DEBUGPY_LAUNCHER_PORT")] = str(launcher_port)
        if log.log_dir is not None:
            env[str("DEBUGPY_LOG_DIR")] = compat.filename_str(log.log_dir)
        if log.stderr.levels != {"warning", "error"}:
            env[str("DEBUGPY_LOG_STDERR")] = str(" ".join(log.stderr.levels))

        if console == "internalConsole":
            log.info("{0} spawning launcher: {1!r}", session, cmdline)
            try:
                # If we are talking to the client over stdio, sys.stdin and sys.stdout
                # are redirected to avoid mangling the DAP message stream. Make sure
                # the launcher also respects that.
                subprocess.Popen(
                    cmdline,
                    env=dict(list(os.environ.items()) + list(env.items())),
                    stdin=sys.stdin,
                    stdout=sys.stdout,
                    stderr=sys.stderr,
                )
            except Exception as exc:
                raise start_request.cant_handle(
                    "{0} failed to spawn {1}: {2}", session, session.launcher, exc
                )
        else:
            log.info('{0} spawning launcher via "runInTerminal" request.', session)
            session.client.capabilities.require("supportsRunInTerminalRequest")
            kinds = {"integratedTerminal": "integrated", "externalTerminal": "external"}
            try:
                session.client.channel.request(
                    "runInTerminal",
                    {
                        "kind": kinds[console],
                        "title": console_title,
                        "args": cmdline,
                        "env": env,
                    },
                )
            except messaging.MessageHandlingError as exc:
                exc.propagate(start_request)

        if not session.wait_for(lambda: session.launcher, timeout=10):
            raise start_request.cant_handle(
                "{0} timed out waiting for {1} to connect", session, session.launcher
            )

        try:
            session.launcher.channel.request(start_request.command, arguments)
        except messaging.MessageHandlingError as exc:
            exc.propagate(start_request)

        if session.no_debug:
            return

        if not session.wait_for(lambda: session.launcher.pid is not None, timeout=10):
            raise start_request.cant_handle(
                '{0} timed out waiting for "process" event from {1}',
                session,
                session.launcher,
            )

        # Wait for the first incoming connection regardless of the PID - it won't
        # necessarily match due to the use of stubs like py.exe or "conda run".
        conn = servers.wait_for_connection(session, lambda conn: True, timeout=10)
        if conn is None:
            raise start_request.cant_handle(
                "{0} timed out waiting for debuggee to spawn", session
            )
        conn.attach_to_session(session)

    finally:
        listener.close()
Example #12
0
 def __str__(self):
     return compat.filename_str(self.path)