Example #1
0
 def long_tmpdir(request, tmpdir):
     """Like tmpdir, but ensures that it's a long rather than short filename on Win32.
     """
     path = compat.filename(tmpdir.strpath)
     buffer = ctypes.create_unicode_buffer(512)
     if GetLongPathNameW(path, buffer, len(buffer)):
         path = buffer.value
     return py.path.local(path)
Example #2
0
    def configure(self, session):
        if self._cwd:
            session.config["cwd"] = self._cwd
            session.config["program"] = self._get_relative_program()
        else:
            session.config["program"] = compat.filename(self.filename.strpath)

        session.config["args"] = self.args
Example #3
0
 def __repr__(self):
     if self._cwd:
         return fmt(
             "program (relative) {0!j} / {1!j}",
             self._cwd,
             self._get_relative_program(),
         )
     else:
         return fmt("program {0!j}", compat.filename(self.filename.strpath))
Example #4
0
def inject(pid, debugpy_args):
    host, port = listener.getsockname()

    cmdline = [
        sys.executable,
        compat.filename(os.path.dirname(debugpy.__file__)),
        "--connect",
        host + ":" + str(port),
    ]
    if adapter.access_token is not None:
        cmdline += ["--adapter-access-token", adapter.access_token]
    cmdline += debugpy_args
    cmdline += ["--pid", str(pid)]

    log.info("Spawning attach-to-PID debugger injector: {0!r}", cmdline)
    try:
        injector = subprocess.Popen(
            cmdline,
            bufsize=0,
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
        )
    except Exception as exc:
        log.swallow_exception(
            "Failed to inject debug server into process with PID={0}", pid
        )
        raise messaging.MessageHandlingError(
            fmt(
                "Failed to inject debug server into process with PID={0}: {1}", pid, exc
            )
        )

    # We need to capture the output of the injector - otherwise it can get blocked
    # on a write() syscall when it tries to print something.

    def capture_output():
        while True:
            line = injector.stdout.readline()
            if not line:
                break
            log.info("Injector[PID={0}] output:\n{1}", pid, line.rstrip())
        log.info("Injector[PID={0}] exited.", pid)

    thread = threading.Thread(
        target=capture_output, name=fmt("Injector[PID={0}] output", pid)
    )
    thread.daemon = True
    thread.start()
Example #5
0
def parse_argv():
    seen = set()
    it = consume_argv()

    while True:
        try:
            arg = next(it)
        except StopIteration:
            raise ValueError("missing target: " + TARGET)

        switch = compat.filename(arg)
        if not switch.startswith("-"):
            switch = ""
        for pattern, placeholder, action in switches:
            if re.match("^(" + pattern + ")$", switch):
                break
        else:
            raise ValueError("unrecognized switch " + switch)

        if switch in seen:
            raise ValueError("duplicate switch " + switch)
        else:
            seen.add(switch)

        try:
            action(arg, it)
        except StopIteration:
            assert placeholder is not None
            raise ValueError(fmt("{0}: missing {1}", switch, placeholder))
        except Exception as exc:
            raise ValueError(
                fmt("invalid {0} {1}: {2}", switch, placeholder, exc))

        if options.target is not None:
            break

    if options.mode is None:
        raise ValueError("either --listen or --connect is required")
    if options.adapter_access_token is not None and options.mode != "connect":
        raise ValueError("--adapter-access-token requires --connect")
    if options.target_kind == "pid" and options.wait_for_client:
        raise ValueError("--pid does not support --wait-for-client")

    assert options.target is not None
    assert options.target_kind is not None
    assert options.address is not None
Example #6
0
    def factory(source):
        assert isinstance(source, types.FunctionType)
        name = source.__name__
        source, _ = inspect.getsourcelines(source)

        # First, find the "def" line.
        def_lineno = 0
        for line in source:
            line = line.strip()
            if line.startswith("def") and line.endswith(":"):
                break
            def_lineno += 1
        else:
            raise ValueError("Failed to locate function header.")

        # Remove everything up to and including "def".
        source = source[def_lineno + 1:]
        assert source

        # Now we need to adjust indentation. Compute how much the first line of
        # the body is indented by, then dedent all lines by that amount. Blank
        # lines don't matter indentation-wise, and might not be indented to begin
        # with, so just replace them with a simple newline.
        line = source[0]
        indent = len(line) - len(line.lstrip())
        source = [l[indent:] if l.strip() else "\n" for l in source]
        source = "".join(source)

        # Write it to file.
        tmpfile = long_tmpdir / (name + ".py")
        tmpfile.strpath = compat.filename(tmpfile.strpath)
        assert not tmpfile.check()
        tmpfile.write(source)

        tmpfile.lines = code.get_marked_line_numbers(tmpfile)
        return tmpfile
Example #7
0
def launch_request(request):
    debug_options = set(request("debugOptions", json.array(unicode)))

    # Handling of properties that can also be specified as legacy "debugOptions" flags.
    # If property is explicitly set to false, but the flag is in "debugOptions", treat
    # it as an error. Returns None if the property wasn't explicitly set either way.
    def property_or_debug_option(prop_name, flag_name):
        assert prop_name[0].islower() and flag_name[0].isupper()

        value = request(prop_name, bool, optional=True)
        if value == ():
            value = None

        if flag_name in debug_options:
            if value is False:
                raise request.isnt_valid(
                    '{0!j}:false and "debugOptions":[{1!j}] are mutually exclusive',
                    prop_name,
                    flag_name,
                )
            value = True

        return value

    python = request("python", json.array(unicode, size=(1,)))
    cmdline = list(python)

    if not request("noDebug", json.default(False)):
        port = request("port", int)
        cmdline += [
            compat.filename(os.path.dirname(debugpy.__file__)),
            "--connect",
            launcher.adapter_host + ":" + str(port),
        ]

        if not request("subProcess", True):
            cmdline += ["--configure-subProcess", "False"]

        qt_mode = request(
            "qt",
            json.enum(
                "none", "auto", "pyside", "pyside2", "pyqt4", "pyqt5", optional=True
            ),
        )
        cmdline += ["--configure-qt", qt_mode]

        adapter_access_token = request("adapterAccessToken", unicode, optional=True)
        if adapter_access_token != ():
            cmdline += ["--adapter-access-token", compat.filename(adapter_access_token)]

        debugpy_args = request("debugpyArgs", json.array(unicode))
        cmdline += debugpy_args

    # Further arguments can come via two channels: the launcher's own command line, or
    # "args" in the request; effective arguments are concatenation of these two in order.
    # Arguments for debugpy (such as -m) always come via CLI, but those specified by the
    # user via "args" are passed differently by the adapter depending on "argsExpansion".
    cmdline += sys.argv[1:]
    cmdline += request("args", json.array(unicode))

    process_name = request("processName", compat.filename(sys.executable))

    env = os.environ.copy()
    env_changes = request("env", json.object((unicode, type(None))))
    if sys.platform == "win32":
        # Environment variables are case-insensitive on Win32, so we need to normalize
        # both dicts to make sure that env vars specified in the debug configuration
        # overwrite the global env vars correctly. If debug config has entries that
        # differ in case only, that's an error.
        env = {k.upper(): v for k, v in os.environ.items()}
        n = len(env_changes)
        env_changes = {k.upper(): v for k, v in env_changes.items()}
        if len(env_changes) != n:
            raise request.isnt_valid('Duplicate entries in "env"')
    if "DEBUGPY_TEST" in env:
        # If we're running as part of a debugpy test, make sure that codecov is not
        # applied to the debuggee, since it will conflict with pydevd.
        env.pop("COV_CORE_SOURCE", None)
    env.update(env_changes)
    env = {k: v for k, v in env.items() if v is not None}

    if request("gevent", False):
        env["GEVENT_SUPPORT"] = "True"

    console = request(
        "console",
        json.enum(
            "internalConsole", "integratedTerminal", "externalTerminal", optional=True
        ),
    )

    redirect_output = property_or_debug_option("redirectOutput", "RedirectOutput")
    if redirect_output is None:
        # If neither the property nor the option were specified explicitly, choose
        # the default depending on console type - "internalConsole" needs it to
        # provide any output at all, but it's unnecessary for the terminals.
        redirect_output = console == "internalConsole"
    if redirect_output:
        # sys.stdout buffering must be disabled - otherwise we won't see the output
        # at all until the buffer fills up.
        env["PYTHONUNBUFFERED"] = "1"
        # Force UTF-8 output to minimize data loss due to re-encoding.
        env["PYTHONIOENCODING"] = "utf-8"

    if property_or_debug_option("waitOnNormalExit", "WaitOnNormalExit"):
        if console == "internalConsole":
            raise request.isnt_valid(
                '"waitOnNormalExit" is not supported for "console":"internalConsole"'
            )
        debuggee.wait_on_exit_predicates.append(lambda code: code == 0)
    if property_or_debug_option("waitOnAbnormalExit", "WaitOnAbnormalExit"):
        if console == "internalConsole":
            raise request.isnt_valid(
                '"waitOnAbnormalExit" is not supported for "console":"internalConsole"'
            )
        debuggee.wait_on_exit_predicates.append(lambda code: code != 0)

    if sys.version_info < (3,):
        # Popen() expects command line and environment to be bytes, not Unicode.
        # Assume that values are filenames - it's usually either that, or numbers -
        # but don't allow encoding to fail if we guessed wrong.
        encode = functools.partial(compat.filename_bytes, errors="replace")
        cmdline = [encode(s) for s in cmdline]
        env = {encode(k): encode(v) for k, v in env.items()}

    debuggee.spawn(process_name, cmdline, env, redirect_output)
    return {}
Example #8
0
    def launch_request(self, request):
        from debugpy.adapter import launchers

        if self.session.id != 1 or len(servers.connections()):
            raise request.cant_handle('"attach" expected')

        debug_options = set(request("debugOptions", json.array(unicode)))

        # Handling of properties that can also be specified as legacy "debugOptions" flags.
        # If property is explicitly set to false, but the flag is in "debugOptions", treat
        # it as an error. Returns None if the property wasn't explicitly set either way.
        def property_or_debug_option(prop_name, flag_name):
            assert prop_name[0].islower() and flag_name[0].isupper()

            value = request(prop_name, bool, optional=True)
            if value == ():
                value = None

            if flag_name in debug_options:
                if value is False:
                    raise request.isnt_valid(
                        '{0!j}:false and "debugOptions":[{1!j}] are mutually exclusive',
                        prop_name,
                        flag_name,
                    )
                value = True

            return value

        # "pythonPath" is a deprecated legacy spelling. If "python" is missing, then try
        # the alternative. But if both are missing, the error message should say "python".
        python_key = "python"
        if python_key in request:
            if "pythonPath" in request:
                raise request.isnt_valid(
                    '"pythonPath" is not valid if "python" is specified')
        elif "pythonPath" in request:
            python_key = "pythonPath"
        python = request(python_key,
                         json.array(unicode, vectorize=True, size=(0, )))
        if not len(python):
            python = [compat.filename(sys.executable)]

        python += request("pythonArgs", json.array(unicode, size=(0, )))
        request.arguments["pythonArgs"] = python[1:]
        request.arguments["python"] = python

        launcher_python = request("debugLauncherPython",
                                  unicode,
                                  optional=True)
        if launcher_python == ():
            launcher_python = python[0]

        program = module = code = ()
        if "program" in request:
            program = request("program", unicode)
            args = [program]
            request.arguments["processName"] = program
        if "module" in request:
            module = request("module", unicode)
            args = ["-m", module]
            request.arguments["processName"] = module
        if "code" in request:
            code = request("code",
                           json.array(unicode, vectorize=True, size=(1, )))
            args = ["-c", "\n".join(code)]
            request.arguments["processName"] = "-c"

        num_targets = len([x for x in (program, module, code) if x != ()])
        if num_targets == 0:
            raise request.isnt_valid(
                'either "program", "module", or "code" must be specified')
        elif num_targets != 1:
            raise request.isnt_valid(
                '"program", "module", and "code" are mutually exclusive')

        # Propagate "args" via CLI if and only if shell expansion is requested.
        args_expansion = request("argsExpansion",
                                 json.enum("shell", "none", optional=True))
        if args_expansion == "shell":
            args += request("args", json.array(unicode))
            request.arguments.pop("args", None)

        cwd = request("cwd", unicode, optional=True)
        if cwd == ():
            # If it's not specified, but we're launching a file rather than a module,
            # and the specified path has a directory in it, use that.
            cwd = None if program == () else (os.path.dirname(program) or None)

        console = request(
            "console",
            json.enum(
                "internalConsole",
                "integratedTerminal",
                "externalTerminal",
                optional=True,
            ),
        )
        console_title = request("consoleTitle",
                                json.default("Python Debug Console"))

        sudo = bool(property_or_debug_option("sudo", "Sudo"))
        if sudo and sys.platform == "win32":
            raise request.cant_handle(
                '"sudo":true is not supported on Windows.')

        launcher_path = request("debugLauncherPath",
                                os.path.dirname(launcher.__file__))
        adapter_host = request("debugAdapterHost", "127.0.0.1")

        servers.serve(adapter_host)
        launchers.spawn_debuggee(
            self.session,
            request,
            [launcher_python],
            launcher_path,
            adapter_host,
            args,
            cwd,
            console,
            console_title,
            sudo,
        )
Example #9
0
 def configure(self, session):
     session.config["program"] = compat.filename(self.filename.strpath)
     session.config["args"] = self.args
Example #10
0
 def __repr__(self):
     return fmt("program {0!j}", compat.filename(self.filename.strpath))
Example #11
0
 def _get_relative_program(self):
     assert self._cwd
     relative_filename = compat.filename(
         self.filename.strpath)[len(self._cwd):]
     assert not relative_filename.startswith(("/", "\\"))
     return relative_filename
Example #12
0
def launch_request(request):
    debug_options = set(request("debugOptions", json.array(unicode)))

    # Handling of properties that can also be specified as legacy "debugOptions" flags.
    # If property is explicitly set to false, but the flag is in "debugOptions", treat
    # it as an error. Returns None if the property wasn't explicitly set either way.
    def property_or_debug_option(prop_name, flag_name):
        assert prop_name[0].islower() and flag_name[0].isupper()

        value = request(prop_name, bool, optional=True)
        if value == ():
            value = None

        if flag_name in debug_options:
            if value is False:
                raise request.isnt_valid(
                    '{0!j}:false and "debugOptions":[{1!j}] are mutually exclusive',
                    prop_name,
                    flag_name,
                )
            value = True

        return value

    # "pythonPath" is a deprecated legacy spelling. If "python" is missing, then try
    # the alternative. But if both are missing, the error message should say "python".
    python_key = "python"
    if python_key in request:
        if "pythonPath" in request:
            raise request.isnt_valid(
                '"pythonPath" is not valid if "python" is specified')
    elif "pythonPath" in request:
        python_key = "pythonPath"
    cmdline = request(python_key,
                      json.array(unicode, vectorize=True, size=(0, )))
    if not len(cmdline):
        cmdline = [compat.filename(sys.executable)]

    if not request("noDebug", json.default(False)):
        port = request("port", int)
        cmdline += [
            compat.filename(os.path.dirname(debugpy.__file__)),
            "--connect",
            str(port),
        ]
        if not request("subProcess", True):
            cmdline += ["--configure-subProcess", "False"]
        adapter_access_token = request("adapterAccessToken",
                                       unicode,
                                       optional=True)
        if adapter_access_token != ():
            cmdline += [
                "--adapter-access-token",
                compat.filename(adapter_access_token)
            ]
        debugpy_args = request("debugpyArgs", json.array(unicode))
        cmdline += debugpy_args

    program = module = code = ()
    if "program" in request:
        program = request("program", unicode)
        cmdline += [program]
        process_name = program
    if "module" in request:
        module = request("module", unicode)
        cmdline += ["-m", module]
        process_name = module
    if "code" in request:
        code = request("code", json.array(unicode, vectorize=True, size=(1, )))
        cmdline += ["-c", "\n".join(code)]
        process_name = cmdline[0]

    num_targets = len([x for x in (program, module, code) if x != ()])
    if num_targets == 0:
        raise request.isnt_valid(
            'either "program", "module", or "code" must be specified')
    elif num_targets != 1:
        raise request.isnt_valid(
            '"program", "module", and "code" are mutually exclusive')

    cmdline += request("args", json.array(unicode))

    cwd = request("cwd", unicode, optional=True)
    if cwd == ():
        # If it's not specified, but we're launching a file rather than a module,
        # and the specified path has a directory in it, use that.
        cwd = None if program == () else (os.path.dirname(program[0]) or None)

    env = os.environ.copy()
    env_changes = request("env", json.object(unicode))
    if sys.platform == "win32":
        # Environment variables are case-insensitive on Win32, so we need to normalize
        # both dicts to make sure that env vars specified in the debug configuration
        # overwrite the global env vars correctly. If debug config has entries that
        # differ in case only, that's an error.
        env = {k.upper(): v for k, v in os.environ.items()}
        n = len(env_changes)
        env_changes = {k.upper(): v for k, v in env_changes.items()}
        if len(env_changes) != n:
            raise request.isnt_valid('Duplicate entries in "env"')
    if "DEBUGPY_TEST" in env:
        # If we're running as part of a debugpy test, make sure that codecov is not
        # applied to the debuggee, since it will conflict with pydevd.
        env.pop("COV_CORE_SOURCE", None)
    env.update(env_changes)

    if request("gevent", False):
        env["GEVENT_SUPPORT"] = "True"

    console = request(
        "console",
        json.enum("internalConsole",
                  "integratedTerminal",
                  "externalTerminal",
                  optional=True),
    )

    redirect_output = property_or_debug_option("redirectOutput",
                                               "RedirectOutput")
    if redirect_output is None:
        # If neither the property nor the option were specified explicitly, choose
        # the default depending on console type - "internalConsole" needs it to
        # provide any output at all, but it's unnecessary for the terminals.
        redirect_output = console == "internalConsole"
    if redirect_output:
        # sys.stdout buffering must be disabled - otherwise we won't see the output
        # at all until the buffer fills up.
        env["PYTHONUNBUFFERED"] = "1"
        # Force UTF-8 output to minimize data loss due to re-encoding.
        env["PYTHONIOENCODING"] = "utf-8"

    if property_or_debug_option("waitOnNormalExit", "WaitOnNormalExit"):
        if console == "internalConsole":
            raise request.isnt_valid(
                '"waitOnNormalExit" is not supported for "console":"internalConsole"'
            )
        debuggee.wait_on_exit_predicates.append(lambda code: code == 0)
    if property_or_debug_option("waitOnAbnormalExit", "WaitOnAbnormalExit"):
        if console == "internalConsole":
            raise request.isnt_valid(
                '"waitOnAbnormalExit" is not supported for "console":"internalConsole"'
            )
        debuggee.wait_on_exit_predicates.append(lambda code: code != 0)

    if sys.version_info < (3, ):
        # Popen() expects command line and environment to be bytes, not Unicode.
        # Assume that values are filenames - it's usually either that, or numbers -
        # but don't allow encoding to fail if we guessed wrong.
        encode = functools.partial(compat.filename_bytes, errors="replace")
        cmdline = [encode(s) for s in cmdline]
        env = {encode(k): encode(v) for k, v in env.items()}

    debuggee.spawn(process_name, cmdline, cwd, env, redirect_output)
    return {}