def _explain_how_realized(self, expectation, reasons): message = fmt("Realized {0!r}", expectation) # For the breakdown, we want to skip any expectations that were exact occurrences, # since there's no point explaining that occurrence was realized by itself. skip = [exp for exp in reasons.keys() if isinstance(exp, Occurrence)] for exp in skip: reasons.pop(exp, None) if reasons == {expectation: some.object}: # If there's only one expectation left to explain, and it's the top-level # one, then we have already printed it, so just add the explanation. reason = reasons[expectation] if "\n" in message: message += fmt(" == {0!r}", reason) else: message += fmt("\n == {0!r}", reason) elif reasons: # Otherwise, break it down expectation by expectation. message += ":" for exp, reason in reasons.items(): message += fmt("\n\n where {0!r}\n == {1!r}", exp, reason) else: message += "." log.info("{0}", message)
def parse(s): n = parser(s) if start is not None and n < start: raise ValueError(fmt("must be >= {0}", start)) if stop is not None and n >= stop: raise ValueError(fmt("must be < {0}", stop)) return n
def srcnameof(obj): """Returns the most descriptive name of a Python module, class, or function, including source information (filename and linenumber), if available. Best-effort, but guaranteed to not fail - always returns something. """ name = nameof(obj, quote=True) # Get the source information if possible. try: src_file = filename(inspect.getsourcefile(obj), "replace") except Exception: pass else: name += fmt(" (file {0!r}", src_file) try: _, src_lineno = inspect.getsourcelines(obj) except Exception: pass else: name += fmt(", line {0}", src_lineno) name += ")" return name
def configure(properties, **kwargs): if _settrace.called: raise RuntimeError("debug adapter is already running") ensure_logging() log.debug("configure{0!r}", (properties, kwargs)) if properties is None: properties = kwargs else: properties = dict(properties) properties.update(kwargs) for k, v in properties.items(): if k not in _config: raise ValueError(fmt("Unknown property {0!r}", k)) expected_type = type(_config[k]) if type(v) is not expected_type: raise ValueError( fmt("{0!r} must be a {1}", k, expected_type.__name__)) valid_values = _config_valid_values.get(k) if (valid_values is not None) and (v not in valid_values): raise ValueError( fmt("{0!r} must be one of: {1!r}", k, valid_values)) _config[k] = v
def test_log_point(pyfile, target, run, condition): @pyfile def code_to_debug(): import debuggee import sys debuggee.setup() for i in range(0, 10): sys.stderr.write(str(i * 10) + "\n") # @bp sys.stderr.flush() () # @wait_for_output lines = code_to_debug.lines with debug.Session() as session: session.config["redirectOutput"] = True with run(session, target(code_to_debug)): bp = {"line": lines["bp"], "logMessage": "{i}"} if condition: bp["condition"] = "i == 5" session.request( "setBreakpoints", { "source": {"path": code_to_debug}, "breakpoints": [bp, {"line": lines["wait_for_output"]}], }, ) if condition: session.wait_for_stop( "breakpoint", expected_frames=[some.dap.frame(code_to_debug, line="bp")] ) var_i = session.get_variable("i") assert var_i == some.dict.containing( {"name": "i", "evaluateName": "i", "type": "int", "value": "5"} ) session.request_continue() session.wait_for_stop( "breakpoint", expected_frames=[some.dap.frame(code_to_debug, line="wait_for_output")], ) session.request_continue() # print() should produce both actual output, and "output" events on stderr, # but logpoints should only produce "output" events on stdout. if "internalConsole" not in str(run): assert not session.captured_stdout() expected_stdout = "".join( (fmt(r"{0}\r?\n", re.escape(str(i))) for i in range(0, 10)) ) expected_stderr = "".join( (fmt(r"{0}\r?\n", re.escape(str(i * 10))) for i in range(0, 10)) ) assert session.output("stdout") == some.str.matching(expected_stdout) assert session.output("stderr") == some.str.matching(expected_stderr)
def describe_circumstances(self): id = collections.OrderedDict(self._id) # Keep it all on one line if it's short enough, but indent longer ones. s = fmt("{0!j:indent=None}", id) if len(s) > SINGLE_LINE_REPR_LIMIT: s = fmt("{0!j}", id) return s
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))
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()
def test_add_and_remove_breakpoint(pyfile, target, run): @pyfile def code_to_debug(): import debuggee debuggee.setup() for i in range(0, 10): print(i) # @bp () # @wait_for_output with debug.Session() as session: session.config["redirectOutput"] = True with run(session, target(code_to_debug)): session.set_breakpoints(code_to_debug, all) session.wait_for_stop( "breakpoint", expected_frames=[some.dap.frame(code_to_debug, line="bp")] ) # Remove breakpoint inside the loop. session.set_breakpoints(code_to_debug, ["wait_for_output"]) session.request_continue() session.wait_for_stop( "breakpoint", expected_frames=[some.dap.frame(code_to_debug, line="wait_for_output")], ) session.request_continue() expected_stdout = "".join((fmt("{0}\n", i) for i in range(0, 10))) assert session.output("stdout") == expected_stdout
def Response(request, body=some.object): assert isinstance(request, Expectation) or isinstance( request, RequestOccurrence) exp = PatternExpectation("response", request, body) exp.timeline = request.timeline exp.has_lower_bound = request.has_lower_bound # Try to be as specific as possible. if isinstance(request, Expectation): if request.circumstances[0] != "request": exp.describe = lambda: fmt("response to {0!r}", request) return else: items = (("command", request.circumstances[1]), ) else: items = (("command", request.command), ) if isinstance(request, Occurrence): items += (("request_seq", request.seq), ) if body is some.object: items += (("\002...", "...\003"), ) elif body is some.error or body == some.error: items += (("success", False), ) if body == some.error: items += (("message", compat.force_str(body)), ) else: items += (("body", body), ) exp.describe = lambda: _describe_message("response", *items) return exp
def __repr__(self): return fmt( "{2}{0}.{1}", self.index, self.describe_circumstances(), "" if self.observed else "*", )
def __init__(self, component, message): """Parses an "initialize" request or response and extracts the feature flags. For every "X" in self.PROPERTIES, sets self["X"] to the corresponding value from message.payload if it's present there, or to the default value otherwise. """ assert message.is_request("initialize") or message.is_response("initialize") self.component = component payload = message.payload for name, validate in self.PROPERTIES.items(): value = payload.get(name, ()) if not callable(validate): validate = json.default(validate) try: value = validate(value) except Exception as exc: raise message.isnt_valid("{0!j} {1}", name, exc) assert value != (), fmt( "{0!j} must provide a default value for missing properties.", validate ) self[name] = value log.debug("{0}", self)
def spawn(process_name, cmdline, env, redirect_output): log.info( "Spawning debuggee process:\n\n" "Command line: {0!r}\n\n" "Environment variables: {1!r}\n\n", cmdline, env, ) close_fds = set() try: if redirect_output: # subprocess.PIPE behavior can vary substantially depending on Python version # and platform; using our own pipes keeps it simple, predictable, and fast. stdout_r, stdout_w = os.pipe() stderr_r, stderr_w = os.pipe() close_fds |= {stdout_r, stdout_w, stderr_r, stderr_w} kwargs = dict(stdout=stdout_w, stderr=stderr_w) else: kwargs = {} try: global process process = subprocess.Popen(cmdline, env=env, bufsize=0, **kwargs) except Exception as exc: raise messaging.MessageHandlingError( fmt("Couldn't spawn debuggee: {0}\n\nCommand line:{1!r}", exc, cmdline) ) log.info("Spawned {0}.", describe()) atexit.register(kill) launcher.channel.send_event( "process", { "startMethod": "launch", "isLocalProcess": True, "systemProcessId": process.pid, "name": process_name, "pointerSize": struct.calcsize(compat.force_str("P")) * 8, }, ) if redirect_output: for category, fd, tee in [ ("stdout", stdout_r, sys.stdout), ("stderr", stderr_r, sys.stderr), ]: output.CaptureOutput(describe(), category, fd, tee) close_fds.remove(fd) wait_thread = threading.Thread(target=wait_for_exit, name="wait_for_exit()") wait_thread.daemon = True wait_thread.start() finally: for fd in close_fds: try: os.close(fd) except Exception: log.swallow_exception()
def __init__(self, name=None): self.name = str(name if name is not None else id(self)) self.ignore_unobserved = [] self._listeners = [] # [(expectation, callable)] self._index_iter = itertools.count(1) self._accepting_new = threading.Event() self._finalized = threading.Event() self._recorded_new = threading.Condition() self._record_queue = queue.Queue() self._recorder_thread = threading.Thread(target=self._recorder_worker, name=fmt( "{0} recorder", self)) self._recorder_thread.daemon = True self._recorder_thread.start() # Set up initial environment for our first mark() self._last = None self._beginning = None self._accepting_new.set() self._beginning = self.mark("START") assert self._last is self._beginning self._proceeding_from = self._beginning
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)
def notify_of_subprocess(self, conn): with self.session: if self.start_request is None or conn in self._known_subprocesses: return if "processId" in self.start_request.arguments: log.warning( "Not reporting subprocess for {0}, because the parent process " 'was attached to using "processId" rather than "port".', self.session, ) return log.info("Notifying {0} about {1}.", self, conn) body = dict(self.start_request.arguments) self._known_subprocesses.add(conn) body.pop("processId", None) body.pop("listen", None) body["name"] = fmt("Subprocess {0}", conn.pid) body["request"] = "attach" body["subProcessId"] = conn.pid host = body.pop("host", None) port = body.pop("port", None) if "connect" not in body: body["connect"] = {} if "host" not in body["connect"]: body["connect"]["host"] = host if host is not None else "127.0.0.1" if "port" not in body["connect"]: if port is None: _, port = listener.getsockname() body["connect"]["port"] = port self.channel.send_event("debugpyAttach", body)
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()
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
def __repr__(self): result = type(self).__name__ args = [str(x) for x in self._args] + [ fmt("{0}={1}", k, v) for k, v in self._kwargs.items() ] if len(args): result += "(" + ", ".join(args) + ")" return result
def pytest_configure(config): if config.option.debugpy_log_dir: log.log_dir = config.option.debugpy_log_dir else: bits = 64 if sys.maxsize > 2 ** 32 else 32 ver = fmt("{0}.{1}-{bits}", *sys.version_info, bits=bits) log.log_dir = (tests.root / "_logs" / ver).strpath log.info("debugpy and pydevd logs will be under {0}", log.log_dir)
def _capture(self, fd, name): assert name not in self._chunks self._chunks[name] = [] thread = threading.Thread(target=lambda: self._worker(fd, name), name=fmt("{0} {1}", self, name)) thread.daemon = True thread.start() self._worker_threads.append(thread)
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}
def set_config(arg, it): prefix = "--configure-" assert arg.startswith(prefix) name = arg[len(prefix) :] value = next(it) if name not in options.config: raise ValueError(fmt("unknown property {0!r}", name)) expected_type = type(options.config[name]) try: if expected_type is bool: value = {"true": True, "false": False}[value.lower()] else: value = expected_type(value) except Exception: raise ValueError(fmt("{0!r} must be a {1}", name, expected_type.__name__)) options.config[name] = value
def _dump_worker_log(command, problem, exc_info=None): reason = fmt("{0}.{1}() {2}", _name, command, problem) if _worker_log_filename is None: reason += ", but there is no log." else: try: with open(_worker_log_filename) as f: worker_log = f.read() except Exception: reason += fmt(", but log {0} could not be retrieved.", _worker_log_filename) else: reason += fmt("; watchdog worker process log:\n\n{0}", worker_log) if exc_info is None: log.error("{0}", reason) else: log.swallow_exception("{0}", reason, exc_info=exc_info) return reason
def __setitem__(self, key, value): """Sets debuggee.scratchpad[key] = value inside the debugged process. """ log.info("{0} debuggee.scratchpad[{1!r}] = {2!r}", self.session, key, value) expr = fmt("sys.modules['debuggee'].scratchpad[{0!r}] = {1!r}", key, value) self.session.request("evaluate", { "context": "repl", "expression": expr })
def _process_event(self, event): occ = self.timeline.record_event(event, block=False) if event.event == "exited": self.observe(occ) self.exit_code = event("exitCode", int) assert self.exit_code == self.expected_exit_code elif event.event == "debugpyAttach": self.observe(occ) pid = event("subProcessId", int) watchdog.register_spawn( pid, fmt("{0}-subprocess-{1}", self.debuggee_id, pid))
def start(self): config = self.config request = config.get("request", None) if request == "attach": host = config["connect"]["host"] port = config["connect"]["port"] self.connect_to_adapter((host, port)) return self.request_attach() else: raise ValueError( fmt('Unsupported "request":{0!j} in session.config', request))
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()
def prefixed(format_string, *args, **kwargs): """Adds a prefix to all messages logged from the current thread for the duration of the context manager. """ prefix = fmt(format_string, *args, **kwargs) old_prefix = getattr(_tls, "prefix", "") _tls.prefix = prefix + old_prefix try: yield finally: _tls.prefix = old_prefix
def _attach_common_config(session, target, cwd): assert target.code is None or "debuggee.setup()" in target.code, fmt( "{0} must invoke debuggee.setup().", target.filename) target.configure(session) config = session.config if cwd is not None: config.setdefault("pathMappings", [{ "localRoot": cwd, "remoteRoot": "." }]) return config