Exemple #1
0
    def attach_request(self, request):
        if self.session.no_debug:
            raise request.isnt_valid('"noDebug" is not supported for "attach"')

        pid = request("processId", int, optional=True)
        if pid == ():
            # When the adapter is spawned by the debug server, it is connected to the
            # latter from the get go, and "host" and "port" in the "attach" request
            # are actually the host and port on which the adapter itself was listening,
            # so we can ignore those.
            if self.server:
                return

            host = request("host", "127.0.0.1")
            port = request("port", int)
            if request("listen", False):
                with self.accept_connection_from_server((host, port)):
                    pass
            else:
                self.session.connect_to_server((host, port))
        else:
            if self.server:
                raise request.isnt_valid(
                    '"attach" with "processId" cannot be serviced by adapter '
                    "that is already associated with a debug server")

            ptvsd_args = request("ptvsdArgs", json.array(unicode))
            self.session.inject_server(pid, ptvsd_args)
Exemple #2
0
    def launch_request(self, request):
        from ptvsd.adapter import launchers

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

        sudo = request("sudo",
                       json.default("Sudo" in self.session.debug_options))
        if sudo:
            if sys.platform == "win32":
                raise request.cant_handle(
                    '"sudo":true is not supported on Windows.')
        else:
            if "Sudo" in self.session.debug_options:
                raise request.isnt_valid(
                    '"sudo":false and "debugOptions":["Sudo"] are mutually exclusive'
                )

        # Launcher doesn't use the command line at all, but we pass the arguments so
        # that they show up in the terminal if we're using "runInTerminal".
        if "program" in request:
            args = request("program",
                           json.array(unicode, vectorize=True, size=(1, )))
        elif "module" in request:
            args = ["-m"] + request(
                "module", json.array(unicode, vectorize=True, size=(1, )))
        elif "code" in request:
            args = ["-c"] + request(
                "code", json.array(unicode, vectorize=True, size=(1, )))
        else:
            args = []
        args += request("args", json.array(unicode))

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

        launchers.spawn_debuggee(self.session, request, sudo, args, console,
                                 console_title)
Exemple #3
0
    def launch_request(self, request):
        sudo = request("sudo",
                       json.default("Sudo" in self.session.debug_options))
        if sudo:
            if platform.system() == "Windows":
                raise request.cant_handle(
                    '"sudo":true is not supported on Windows.')
        else:
            if "Sudo" in self.session.debug_options:
                raise request.isnt_valid(
                    '"sudo":false and "debugOptions":["Sudo"] are mutually exclusive'
                )

        # Launcher doesn't use the command line at all, but we pass the arguments so
        # that they show up in the terminal if we're using "runInTerminal".
        if "program" in request:
            args = request("program",
                           json.array(unicode, vectorize=True, size=(1, )))
        elif "module" in request:
            args = ["-m"] + request(
                "module", json.array(unicode, vectorize=True, size=(1, )))
        elif "code" in request:
            args = ["-c"] + request(
                "code", json.array(unicode, vectorize=True, size=(1, )))
        else:
            args = []
        args += request("args", json.array(unicode))

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

        self.session.spawn_debuggee(request, sudo, args, console,
                                    console_title)

        if "RedirectOutput" in self.session.debug_options:
            # The launcher is doing output redirection, so we don't need the server.
            request.arguments["debugOptions"].remove("RedirectOutput")
Exemple #4
0
    def get_variables(self, *varnames, **kwargs):
        """Fetches the specified variables from the frame specified by frame_id, or
        from the topmost frame in the last "stackTrace" response if frame_id is not
        specified.

        If varnames is empty, then all variables in the frame are returned. The result
        is an OrderedDict, in which every entry has variable name as the key, and a
        DAP Variable object as the value. The original order of variables as reported
        by the debugger is preserved.

        If varnames is not empty, then only the specified variables are returned.
        The result is a tuple, in which every entry is a DAP Variable object; those
        entries are in the same order as varnames.
        """

        assert self.timeline.is_frozen

        frame_id = kwargs.pop("frame_id", None)
        if frame_id is None:
            stackTrace_responses = self.all_occurrences_of(
                timeline.Response(timeline.Request("stackTrace")))
            assert stackTrace_responses, (
                "get_variables() without frame_id requires at least one response "
                'to a "stackTrace" request in the timeline.')
            stack_trace = stackTrace_responses[-1]
            frame_id = stack_trace.body.get("stackFrames",
                                            json.array())[0]("id", int)

        scopes = self.request("scopes", {"frameId": frame_id})("scopes",
                                                               json.array())
        assert len(scopes) > 0

        variables = self.request(
            "variables",
            {"variablesReference": scopes[0]("variablesReference", int)})(
                "variables", json.array())

        variables = collections.OrderedDict(
            ((v("name", unicode), v) for v in variables))
        if varnames:
            assert set(varnames) <= set(variables.keys())
            return tuple((variables[name] for name in varnames))
        else:
            return variables
Exemple #5
0
        def handle(self, request):
            assert request.is_request("launch", "attach")
            if self._initialize_request is None:
                raise request.isnt_valid("Session is not initialized yet")
            if self.launcher:
                raise request.isnt_valid("Session is already started")

            self.session.no_debug = request("noDebug", json.default(False))
            self.session.debug_options = set(
                request("debugOptions", json.array(unicode)))

            f(self, request)

            if self.server:
                self.server.initialize(self._initialize_request)
                self._initialize_request = None

                # pydevd doesn't send "initialized", and responds to the start request
                # immediately, without waiting for "configurationDone". If it changes
                # to conform to the DAP spec, we'll need to defer waiting for response.
                self.server.channel.delegate(request)

            if self.session.no_debug:
                request.respond({})
                self._propagate_deferred_events()
                return

            if {"WindowsClient", "Windows"} & self.session.debug_options:
                client_os_type = "WINDOWS"
            elif {"UnixClient", "UNIX"} & self.session.debug_options:
                client_os_type = "UNIX"
            else:
                client_os_type = "WINDOWS" if platform.system(
                ) == "Windows" else "UNIX"
            self.server.channel.request(
                "setDebuggerProperty",
                {
                    "skipSuspendOnBreakpointException": ("BaseException", ),
                    "skipPrintBreakpointException": ("NameError", ),
                    "multiThreadsSingleNotification": True,
                    "ideOS": client_os_type,
                },
            )

            # Let the IDE know that it can begin configuring the adapter.
            self.channel.send_event("initialized")

            self._start_request = request
            return messaging.NO_RESPONSE  # will respond on "configurationDone"
Exemple #6
0
 def _process_request(self, request):
     self.timeline.record_request(request, block=False)
     if request.command == "runInTerminal":
         args = request("args", json.array(unicode))
         cwd = request("cwd", ".")
         env = request("env", json.object(unicode))
         try:
             exe = args.pop(0)
             assert not len(self.spawn_debuggee.env)
             self.spawn_debuggee.env = env
             self.spawn_debuggee(args, cwd, exe=exe)
             return {}
         except OSError as exc:
             log.exception('"runInTerminal" failed:')
             raise request.cant_handle(str(exc))
     else:
         raise request.isnt_valid("not supported")
Exemple #7
0
    def set_breakpoints(self, path, lines):
        """Sets breakpoints in the specified file, and returns the list of all the
        corresponding DAP Breakpoint objects in the same order.

        If lines are specified, it should be an iterable in which every element is
        either a line number or a string. If it is a string, then it is translated
        to the corresponding line number via get_marked_line_numbers(path).

        If lines=all, breakpoints will be set on all the marked lines in the file.
        """

        # Don't fetch line markers unless needed - in some cases, the breakpoints
        # might be set in a file that does not exist on disk (e.g. remote attach).
        get_marked_line_numbers = lambda: code.get_marked_line_numbers(path)

        if lines is all:
            lines = get_marked_line_numbers().keys()

        def make_breakpoint(line):
            if isinstance(line, int):
                descr = str(line)
            else:
                marker = line
                line = get_marked_line_numbers()[marker]
                descr = fmt("{0} (@{1})", line, marker)
            bp_log.append((line, descr))
            return {"line": line}

        bp_log = []
        breakpoints = self.request(
            "setBreakpoints",
            {
                "source": {
                    "path": path
                },
                "breakpoints": [make_breakpoint(line) for line in lines],
            },
        )("breakpoints", json.array())

        bp_log = sorted(bp_log, key=lambda pair: pair[0])
        bp_log = ", ".join((descr for _, descr in bp_log))
        log.info("Breakpoints set in {0}: {1}", path, bp_log)

        return breakpoints
Exemple #8
0
    def wait_for_stop(
        self,
        reason=some.str,
        expected_frames=None,
        expected_text=None,
        expected_description=None,
    ):
        stopped = self.wait_for_next_event("stopped")

        expected_stopped = {
            "reason": reason,
            "threadId": some.int,
            "allThreadsStopped": True,
        }
        if expected_text is not None:
            expected_stopped["text"] = expected_text
        if expected_description is not None:
            expected_stopped["description"] = expected_description
        if stopped("reason", unicode) not in [
                "step",
                "exception",
                "breakpoint",
                "entry",
                "goto",
        ]:
            expected_stopped["preserveFocusHint"] = True
        assert stopped == some.dict.containing(expected_stopped)

        tid = stopped("threadId", int)
        stack_trace = self.request("stackTrace", {"threadId": tid})
        frames = stack_trace("stackFrames", json.array()) or []
        assert len(frames) == stack_trace("totalFrames", int)

        if expected_frames:
            assert len(expected_frames) <= len(frames)
            assert expected_frames == frames[0:len(expected_frames)]

        fid = frames[0]("id", int)
        return StopInfo(stopped, frames, tid, fid)
Exemple #9
0
    def attach_request(self, request):
        if self.session.no_debug:
            raise request.isnt_valid('"noDebug" is not supported for "attach"')

        # There are four distinct possibilities here.
        #
        # If "processId" is specified, this is attach-by-PID. We need to inject the
        # debug server into the designated process, and then wait until it connects
        # back to us. Since the injected server can crash, there must be a timeout.
        #
        # If "subProcessId" is specified, this is attach to a known subprocess, likely
        # in response to a "ptvsd_attach" event. If so, the debug server should be
        # connected already, and thus the wait timeout is zero.
        #
        # If neither is specified, and "waitForAttach" is true, this is attach-by-socket
        # with the server expected to connect to the adapter via ptvsd.attach(). There
        # is no PID known in advance, so just wait until the first server connection
        # indefinitely, with no timeout.
        #
        # If neither is specified, and "waitForAttach" is false, this is attach-by-socket
        # in which the server has spawned the adapter via ptvsd.enable_attach(). There
        # is no PID known to the IDE in advance, but the server connection should be
        # either be there already, or the server should be connecting shortly, so there
        # must be a timeout.
        #
        # In the last two cases, if there's more than one server connection already,
        # this is a multiprocess re-attach. The IDE doesn't know the PID, so we just
        # connect it to the oldest server connection that we have - in most cases, it
        # will be the one for the root debuggee process, but if it has exited already,
        # it will be some subprocess.

        pid = request("processId", (int, unicode), optional=True)
        sub_pid = request("subProcessId", int, optional=True)
        if pid != ():
            if sub_pid != ():
                raise request.isnt_valid(
                    '"processId" and "subProcessId" are mutually exclusive')
            if not isinstance(pid, int):
                try:
                    pid = int(pid)
                except Exception:
                    raise request.isnt_valid(
                        '"processId" must be parseable as int')
            ptvsd_args = request("ptvsdArgs", json.array(unicode))
            servers.inject(pid, ptvsd_args)
            timeout = 10
            pred = lambda conn: conn.pid == pid
        else:
            if sub_pid == ():
                pred = lambda conn: True
                timeout = None if request("waitForAttach", False) else 10
            else:
                pred = lambda conn: conn.pid == sub_pid
                timeout = 0

        conn = servers.wait_for_connection(self.session, pred, timeout)
        if conn is None:
            raise request.cant_handle(
                ("Timed out waiting for debug server to connect." if timeout
                 else "There is no debug server connected to this adapter."
                 if sub_pid == () else
                 'No known subprocess with "subProcessId":{0}'),
                sub_pid,
            )

        try:
            conn.attach_to_session(self.session)
        except ValueError:
            request.cant_handle("{0} is already being debugged.", conn)
Exemple #10
0
        def handle(self, request):
            assert request.is_request("launch", "attach")
            if self._initialize_request is None:
                raise request.isnt_valid("Session is not initialized yet")
            if self.launcher or self.server:
                raise request.isnt_valid("Session is already started")

            self.session.no_debug = request("noDebug", json.default(False))
            if self.session.no_debug:
                servers.dont_wait_for_first_connection()

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

            f(self, request)

            if self.server:
                self.server.initialize(self._initialize_request)
                self._initialize_request = None

                arguments = request.arguments
                if self.launcher:
                    if "RedirectOutput" in debug_options:
                        # The launcher is doing output redirection, so we don't need the
                        # server to do it, as well.
                        arguments = dict(arguments)
                        arguments["debugOptions"] = list(debug_options -
                                                         {"RedirectOutput"})

                    if arguments.get("redirectOutput"):
                        arguments = dict(arguments)
                        del arguments["redirectOutput"]

                # pydevd doesn't send "initialized", and responds to the start request
                # immediately, without waiting for "configurationDone". If it changes
                # to conform to the DAP spec, we'll need to defer waiting for response.
                try:
                    self.server.channel.request(request.command, arguments)
                except messaging.MessageHandlingError as exc:
                    exc.propagate(request)

            if self.session.no_debug:
                self.start_request = request
                self.has_started = True
                request.respond({})
                self._propagate_deferred_events()
                return

            if {"WindowsClient", "Windows"} & debug_options:
                client_os_type = "WINDOWS"
            elif {"UnixClient", "UNIX"} & debug_options:
                client_os_type = "UNIX"
            else:
                client_os_type = "WINDOWS" if sys.platform == "win32" else "UNIX"
            self.server.channel.request(
                "setDebuggerProperty",
                {
                    "skipSuspendOnBreakpointException": ("BaseException", ),
                    "skipPrintBreakpointException": ("NameError", ),
                    "multiThreadsSingleNotification": True,
                    "ideOS": client_os_type,
                },
            )

            # Let the IDE know that it can begin configuring the adapter.
            self.channel.send_event("initialized")

            self.start_request = request
            return messaging.NO_RESPONSE  # will respond on "configurationDone"
Exemple #11
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

    cmdline = []
    if property_or_debug_option("sudo", "Sudo"):
        if sys.platform == "win32":
            raise request.cant_handle('"sudo":true is not supported on Windows.')
        else:
            cmdline += ["sudo"]

    # "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=(1,)))
    if not len(python):
        python = [compat.filename(sys.executable)]
    cmdline += python

    if not request("noDebug", json.default(False)):
        port = request("port", int)
        cmdline += [
            compat.filename(os.path.dirname(ptvsd.__file__)),
            "--client",
            "--host",
            "127.0.0.1",
            "--port",
            str(port),
        ]
        client_access_token = request("clientAccessToken", unicode, optional=True)
        if client_access_token != ():
            cmdline += ["--client-access-token", compat.filename(client_access_token)]
        ptvsd_args = request("ptvsdArgs", json.array(unicode))
        cmdline += ptvsd_args

    program = module = code = ()
    if "program" in request:
        program = request("program", json.array(unicode, vectorize=True, size=(1,)))
        cmdline += program
        process_name = program[0]
    if "module" in request:
        module = request("module", json.array(unicode, vectorize=True, size=(1,)))
        cmdline += ["-m"] + module
        process_name = module[0]
    if "code" in request:
        code = request("code", json.array(unicode, vectorize=True, size=(1,)))
        cmdline += ["-c"] + code
        process_name = python[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()
    if "PTVSD_TEST" in env:
        # If we're running as part of a ptvsd 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(request("env", json.object(unicode)))

    if request("gevent", False):
        env["GEVENT_SUPPORT"] = "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 = request("console", unicode) == "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"):
        debuggee.wait_on_exit_predicates.append(lambda code: code == 0)
    if property_or_debug_option("waitOnAbnormalExit", "WaitOnAbnormalExit"):
        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 {}