def spawn_adapter(self, args=()): assert self.adapter is None assert self.channel is None args = [sys.executable, os.path.dirname(debugpy.adapter.__file__)] + list(args) env = self._make_env(self.spawn_adapter.env) log.info( "Spawning {0}:\n\n" "Command line: {1!j}\n\n" "Environment variables: {2!j}\n\n", self.adapter_id, args, env, ) self.adapter = psutil.Popen( args, bufsize=0, stdin=subprocess.PIPE, stdout=subprocess.PIPE, env=env.for_popen(), ) log.info("Spawned {0} with PID={1}", self.adapter_id, self.adapter.pid) watchdog.register_spawn(self.adapter.pid, self.adapter_id) stream = messaging.JsonIOStream.from_process(self.adapter, name=self.adapter_id) self._start_channel(stream)
def _propagate_deferred_events(self): log.debug("Propagating deferred events to {0}...", self.client) for event in self._deferred_events: log.debug("Propagating deferred {0}", event.describe()) self.client.channel.propagate(event) log.info("All deferred events propagated to {0}.", self.client) self._deferred_events = None
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 wait_for_connection(session, predicate, timeout=None): """Waits until there is a server with the specified PID connected to this adapter, and returns the corresponding Connection. If there is more than one server connection already available, returns the oldest one. """ def wait_for_timeout(): time.sleep(timeout) wait_for_timeout.timed_out = True with _lock: _connections_changed.set() wait_for_timeout.timed_out = timeout == 0 if timeout: thread = threading.Thread( target=wait_for_timeout, name="servers.wait_for_connection() timeout" ) thread.daemon = True thread.start() if timeout != 0: log.info("{0} waiting for connection from debug server...", session) while True: with _lock: _connections_changed.clear() conns = (conn for conn in _connections if predicate(conn)) conn = next(conns, None) if conn is not None or wait_for_timeout.timed_out: return conn _connections_changed.wait()
def wait_for_remaining_output(): """Waits for all remaining output to be captured and propagated. """ for category, instance in CaptureOutput.instances.items(): log.info("Waiting for remaining {0} of {1}.", category, instance._whose) instance._worker_thread.join()
def debug(address, **kwargs): if _settrace.called: raise RuntimeError("this process already has a debug adapter") try: _, port = address except Exception: port = address address = ("127.0.0.1", port) try: port.__index__() # ensure it's int-like except Exception: raise ValueError("expected port or (host, port)") if not (0 <= port < 2**16): raise ValueError("invalid port number") ensure_logging() log.debug("{0}({1!r}, **{2!r})", func.__name__, address, kwargs) log.info("Initial debug configuration: {0!j}", _config) settrace_kwargs = { "suspend": False, "patch_multiprocessing": _config.get("subProcess", True), } debugpy_path, _, _ = get_abs_path_real_path_and_base_from_file( debugpy.__file__) debugpy_path = os.path.dirname(debugpy_path) settrace_kwargs["dont_trace_start_patterns"] = (debugpy_path, ) settrace_kwargs["dont_trace_end_patterns"] = ("debugpy_launcher.py", ) try: return func(address, settrace_kwargs, **kwargs) except Exception: log.reraise_exception("{0}() failed:", func.__name__, level="info")
def main(): original_argv = list(sys.argv) try: parse_argv() except Exception as exc: print(HELP + "\nError: " + str(exc), file=sys.stderr) sys.exit(2) if options.log_to is not None: debugpy.log_to(options.log_to) if options.log_to_stderr: debugpy.log_to(sys.stderr) api.ensure_logging() log.info( "sys.argv before parsing: {0!r}\n" " after parsing: {1!r}", original_argv, sys.argv, ) try: run = { "file": run_file, "module": run_module, "code": run_code, "pid": attach_to_pid, }[options.target_kind] run() except SystemExit as exc: log.reraise_exception("Debuggee exited via SystemExit: {0!r}", exc.code, level="debug")
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 accept_worker(): log.info( "Listening for incoming connection from {0} on port {1}...", self, self.port, ) server_socket = self._server_socket if server_socket is None: return # concurrent close() try: sock, _ = server_socket.accept() except socket.timeout: if self._server_socket is None: return else: log.reraise_exception( "Timed out waiting for {0} to connect", self) except Exception: if self._server_socket is None: return else: log.reraise_exception( "Error accepting connection for {0}:", self) sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) log.info("Incoming connection from {0} accepted.", self) self._socket = sock self._setup_stream()
def wait_for_exit(): try: code = process.wait() if sys.platform != "win32" and code < 0: # On POSIX, if the process was terminated by a signal, Popen will use # a negative returncode to indicate that - but the actual exit code of # the process is always an unsigned number, and can be determined by # taking the lowest 8 bits of that negative returncode. code &= 0xFF except Exception: log.swallow_exception("Couldn't determine process exit code") code = -1 log.info("{0} exited with code {1}", describe(), code) output.wait_for_remaining_output() # Determine whether we should wait or not before sending "exited", so that any # follow-up "terminate" requests don't affect the predicates. should_wait = any(pred(code) for pred in wait_on_exit_predicates) try: launcher.channel.send_event("exited", {"exitCode": code}) except Exception: pass if should_wait: _wait_for_user_input() try: launcher.channel.send_event("terminated") except Exception: pass
def _send_requests_and_events(self, channel): types = [ random.choice(("event", "request")) for _ in range(0, 100) ] for typ in types: name = random.choice(("fizz", "buzz", "fizzbuzz")) body = random.randint(0, 100) with self.lock: self.sent.append((typ, name, body)) if typ == "event": channel.send_event(name, body) elif typ == "request": req = channel.send_request(name, body) req.on_response(self._got_response) channel.send_event("done") # Spin until we receive "done", and also get responses to all requests. requests_sent = types.count("request") log.info("{0} waiting for {1} responses...", self.name, requests_sent) while True: with self.lock: if self.done: if requests_sent == len(self.responses_received): break time.sleep(0.1)
def finalize(self, why, terminate_debuggee=None): """Finalizes the debug session. If the server is present, sends "disconnect" request with "terminateDebuggee" set as specified request to it; waits for it to disconnect, allowing any remaining messages from it to be handled; and closes the server channel. If the launcher is present, sends "terminate" request to it, regardless of the value of terminate; waits for it to disconnect, allowing any remaining messages from it to be handled; and closes the launcher channel. If the client is present, sends "terminated" event to it. If terminate_debuggee=None, it is treated as True if the session has a Launcher component, and False otherwise. """ if self.is_finalizing: return self.is_finalizing = True log.info("{0}; finalizing {1}.", why, self) if terminate_debuggee is None: terminate_debuggee = bool(self.launcher) try: self._finalize(why, terminate_debuggee) except Exception: # Finalization should never fail, and if it does, the session is in an # indeterminate and likely unrecoverable state, so just fail fast. log.swallow_exception("Fatal error while finalizing {0}", self) os._exit(1) log.info("{0} finalized.", self)
def launch(session, target, console=None, cwd=None): assert console in (None, "internalConsole", "integratedTerminal", "externalTerminal") log.info("Launching {0} in {1} using {2!j}.", target, session, console) target.configure(session) config = session.config config.setdefaults({ "console": "externalTerminal", "internalConsoleOptions": "neverOpen" }) if console is not None: config["console"] = console if cwd is not None: config["cwd"] = cwd if "python" not in config and "pythonPath" not in config: config["python"] = sys.executable env = (session.spawn_adapter.env if config["console"] == "internalConsole" else config.env) target.cli(env) session.spawn_adapter() return session.request_launch()
def main(): from debugpy import launcher from debugpy.common import log from debugpy.launcher import debuggee log.to_file(prefix="debugpy.launcher") log.describe_environment("debugpy.launcher startup environment:") # Disable exceptions on Ctrl+C - we want to allow the debuggee process to handle # these, or not, as it sees fit. If the debuggee exits on Ctrl+C, the launcher # will also exit, so it doesn't need to observe the signal directly. signal.signal(signal.SIGINT, signal.SIG_IGN) # Everything before "--" is command line arguments for the launcher itself, # and everything after "--" is command line arguments for the debuggee. log.info("sys.argv before parsing: {0}", sys.argv) sep = sys.argv.index("--") launcher_argv = sys.argv[1:sep] sys.argv[:] = [sys.argv[0]] + sys.argv[sep + 1:] log.info("sys.argv after patching: {0}", sys.argv) # The first argument specifies the host/port on which the adapter is waiting # for launcher to connect. It's either host:port, or just port. adapter = launcher_argv[0] host, sep, port = adapter.partition(":") if not sep: host = "127.0.0.1" port = adapter port = int(port) launcher.connect(host, port) launcher.channel.wait() if debuggee.process is not None: sys.exit(debuggee.process.returncode)
def __init__(self, whose, category, fd, stream): assert category not in self.instances self.instances[category] = self log.info("Capturing {0} of {1}.", category, whose) self.category = category self._whose = whose self._fd = fd self._decoder = codecs.getincrementaldecoder("utf-8")( errors="surrogateescape") if stream is None: # Can happen if running under pythonw.exe. self._stream = None else: self._stream = stream if sys.version_info < ( 3, ) else stream.buffer encoding = stream.encoding if encoding is None or encoding == "cp65001": encoding = "utf-8" try: self._encode = codecs.getencoder(encoding) except Exception: log.swallow_exception( "Unsupported {0} encoding {1!r}; falling back to UTF-8.", category, encoding, level="warning", ) self._encode = codecs.getencoder("utf-8") self._worker_thread = threading.Thread(target=self._worker, name=category) self._worker_thread.start()
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)
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 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 delete_listener_file(): log.info("Listener ports closed; deleting {0!r}", listener_file) try: os.remove(listener_file) except Exception: log.swallow_exception("Failed to delete {0!r}", listener_file, level="warning")
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 test_docstrings(): for attr in debugpy.__all__: log.info("Checking docstring for debugpy.{0}", attr) member = getattr(debugpy, attr) doc = inspect.getdoc(member) for lineno, line in enumerate(doc.split("\n")): assert len(line) <= 72
def wait_until_realized(self, expectation, freeze=None, explain=True, observe=True): if explain: log.info("Waiting for {0!r}", expectation) return self._wait_until_realized(expectation, freeze, explain, observe)
def wait_for_next(self, expectation, freeze=True, explain=True, observe=True): if explain: log.info("Waiting for next {0!r}", expectation) return self._wait_until_realized(self._proceeding_from >> expectation, freeze, explain, observe)
def kill(): if process is None: return try: if process.poll() is None: log.info("Killing {0}", describe()) process.kill() except Exception: log.swallow_exception("Failed to kill {0}", describe())
def __init__(self, session, **fds): self.session = session self._lock = threading.Lock() self._chunks = {} self._worker_threads = [] for stream_name, fd in fds.items(): log.info("Capturing {0} {1}", session.debuggee_id, stream_name) self._capture(fd, stream_name)
def wait_for(self, expectation, freeze=None, explain=True): assert expectation.has_lower_bound, ( "Expectation must have a lower time bound to be used with wait_for()! " "Use >> to sequence an expectation against an occurrence to establish a lower bound, " "or wait_for_next() to wait for the next expectation since the timeline was last " "frozen, or wait_until_realized() when a lower bound is really not necessary." ) if explain: log.info("Waiting for {0!r}", expectation) return self._wait_until_realized(expectation, freeze, explain=explain)
def connect_to_adapter(self, address): assert self.channel is None host, port = address log.info("Connecting to {0} at {1}:{2}", self.adapter_id, host, port) sock = sockets.create_client() sock.connect(address) stream = messaging.JsonIOStream.from_socket(sock, name=self.adapter_id) self._start_channel(stream)
def attach_to_session(self, session): """Attaches this server to the specified Session as a Server component. Raises ValueError if the server already belongs to some session. """ with _lock: if self.server is not None: raise ValueError log.info("Attaching {0} to {1}", self, session) self.server = Server(session, self)
def run_code(): # Add current directory to path, like Python itself does for -c. sys.path.insert(0, "") code = compile(options.target, "<string>", "exec") start_debugging("-c") log.describe_environment("Pre-launch environment:") log.info("Running code:\n\n{0}", options.target) eval(code, {})
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()