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 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 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 serve(name, handler, host, port=0, backlog=socket.SOMAXCONN, timeout=None): """Accepts TCP connections on the specified host and port, and invokes the provided handler function for every new connection. Returns the created server socket. """ assert backlog > 0 try: listener = create_server(host, port, backlog, timeout) except Exception: log.reraise_exception( "Error listening for incoming {0} connections on {1}:{2}:", name, host, port) host, port = listener.getsockname() log.info("Listening for incoming {0} connections on {1}:{2}...", name, host, port) def accept_worker(): while True: try: sock, (other_host, other_port) = listener.accept() except (OSError, socket.error): # Listener socket has been closed. break log.info( "Accepted incoming {0} connection from {1}:{2}.", name, other_host, other_port, ) handler(sock) thread = threading.Thread(target=accept_worker) thread.daemon = True thread.pydev_do_not_trace = True thread.is_pydev_daemon_thread = True thread.start() return listener
def attach_to_pid(): pid = options.target log.info("Attaching to process with PID={0}", pid) encode = lambda s: list(bytearray(s.encode("utf-8")) ) if s is not None else None script_dir = os.path.dirname(debugpy.server.__file__) assert os.path.exists(script_dir) script_dir = encode(script_dir) setup = { "mode": options.mode, "address": options.address, "wait_for_client": options.wait_for_client, "log_to": options.log_to, "adapter_access_token": options.adapter_access_token, } setup = encode(json.dumps(setup)) python_code = """ import codecs; import json; import sys; decode = lambda s: codecs.utf_8_decode(bytearray(s))[0] if s is not None else None; script_dir = decode({script_dir}); setup = json.loads(decode({setup})); sys.path.insert(0, script_dir); import attach_pid_injected; del sys.path[0]; attach_pid_injected.attach(setup); """ python_code = (python_code.replace("\r", "").replace("\n", "").format( script_dir=script_dir, setup=setup)) log.info("Code to be injected: \n{0}", python_code.replace(";", ";\n")) # pydevd restriction on characters in injected code. assert not ( {'"', "'", "\r", "\n"} & set(python_code) ), "Injected code should not contain any single quotes, double quotes, or newlines." pydevd_attach_to_process_path = os.path.join( os.path.dirname(pydevd.__file__), "pydevd_attach_to_process") assert os.path.exists(pydevd_attach_to_process_path) sys.path.append(pydevd_attach_to_process_path) try: import add_code_to_python_process # noqa log.info("Injecting code into process with PID={0} ...", pid) add_code_to_python_process.run_python_code( pid, python_code, connect_debugger_tracing=True, show_debug_info=int( os.getenv("DEBUGPY_ATTACH_BY_PID_DEBUG_INFO", "0")), ) except Exception: log.reraise_exception("Code injection into PID={0} failed:", pid) log.info("Code injection into PID={0} completed.", pid)
def main(args): # If we're talking DAP over stdio, stderr is not guaranteed to be read from, # so disable it to avoid the pipe filling and locking up. This must be done # as early as possible, before the logging module starts writing to it. if args.port is None: sys.stderr = open(os.devnull, "w") from debugpy import adapter from debugpy.common import compat, log, sockets from debugpy.adapter import clients, servers, sessions if args.for_server is not None: if os.name == "posix": # On POSIX, we need to leave the process group and its session, and then # daemonize properly by double-forking (first fork already happened when # this process was spawned). os.setsid() if os.fork() != 0: sys.exit(0) for stdio in sys.stdin, sys.stdout, sys.stderr: if stdio is not None: stdio.close() if args.log_stderr: log.stderr.levels |= set(log.LEVELS) if args.log_dir is not None: log.log_dir = args.log_dir log.to_file(prefix="debugpy.adapter") log.describe_environment("debugpy.adapter startup environment:") servers.access_token = args.server_access_token if args.for_server is None: adapter.access_token = compat.force_str( codecs.encode(os.urandom(32), "hex")) endpoints = {} try: client_host, client_port = clients.serve(args.host, args.port) except Exception as exc: if args.for_server is None: raise endpoints = { "error": "Can't listen for client connections: " + str(exc) } else: endpoints["client"] = {"host": client_host, "port": client_port} if args.for_server is not None: try: server_host, server_port = servers.serve() except Exception as exc: endpoints = { "error": "Can't listen for server connections: " + str(exc) } else: endpoints["server"] = {"host": server_host, "port": server_port} log.info( "Sending endpoints info to debug server at localhost:{0}:\n{1!j}", args.for_server, endpoints, ) try: sock = sockets.create_client() try: sock.settimeout(None) sock.connect(("127.0.0.1", args.for_server)) sock_io = sock.makefile("wb", 0) try: sock_io.write(json.dumps(endpoints).encode("utf-8")) finally: sock_io.close() finally: sockets.close_socket(sock) except Exception: log.reraise_exception( "Error sending endpoints info to debug server:") if "error" in endpoints: log.error("Couldn't set up endpoints; exiting.") sys.exit(1) listener_file = os.getenv("DEBUGPY_ADAPTER_ENDPOINTS") if listener_file is not None: log.info("Writing endpoints info to {0!r}:\n{1!j}", listener_file, endpoints) 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") try: with open(listener_file, "w") as f: atexit.register(delete_listener_file) print(json.dumps(endpoints), file=f) except Exception: log.reraise_exception("Error writing endpoints info to file:") if args.port is None: clients.Client("stdio") # These must be registered after the one above, to ensure that the listener sockets # are closed before the endpoint info file is deleted - this way, another process # can wait for the file to go away as a signal that the ports are no longer in use. atexit.register(servers.stop_serving) atexit.register(clients.stop_serving) servers.wait_until_disconnected() log.info( "All debug servers disconnected; waiting for remaining sessions...") sessions.wait_until_ended() log.info("All debug sessions have ended; exiting.")
def option(name, type, *args): try: return type(os.environ.pop(name, *args)) except Exception: log.reraise_exception("Error parsing {0!r}:", name)
def report(): try: raise ComponentNotAvailable(type) except Exception as exc: log.reraise_exception("{0} in {1}", exc, session)
def attach(setup): log = None try: import sys if "threading" not in sys.modules: try: def on_warn(msg): print(msg, file=sys.stderr) def on_exception(msg): print(msg, file=sys.stderr) def on_critical(msg): print(msg, file=sys.stderr) pydevd_attach_to_process_path = os.path.join( _debugpy_dir, "debugpy", "_vendored", "pydevd", "pydevd_attach_to_process", ) assert os.path.exists(pydevd_attach_to_process_path) sys.path.insert(0, pydevd_attach_to_process_path) # NOTE: that it's not a part of the pydevd PYTHONPATH import attach_script attach_script.fix_main_thread_id(on_warn=on_warn, on_exception=on_exception, on_critical=on_critical) # NOTE: At this point it should be safe to remove this. sys.path.remove(pydevd_attach_to_process_path) except: import traceback traceback.print_exc() raise sys.path.insert(0, _debugpy_dir) try: import debugpy import debugpy.server from debugpy.common import log import pydevd finally: assert sys.path[0] == _debugpy_dir del sys.path[0] py_db = pydevd.get_global_debugger() if py_db is not None: py_db.dispose_and_kill_all_pydevd_threads(wait=False) if setup["log_to"] is not None: debugpy.log_to(setup["log_to"]) log.info("Configuring injected debugpy: {0!j}", setup) if setup["mode"] == "listen": debugpy.listen(setup["address"]) elif setup["mode"] == "connect": debugpy.connect(setup["address"], access_token=setup["adapter_access_token"]) else: raise AssertionError(repr(setup)) except: import traceback traceback.print_exc() if log is None: raise else: log.reraise_exception() log.info("debugpy injected successfully")
def main(tests_pid): # To import debugpy, the "" entry in sys.path - which is added automatically on # Python 2 - must be removed first; otherwise, we end up importing tests/debugpy. if "" in sys.path: sys.path.remove("") from debugpy.common import fmt, log, messaging # log.stderr_levels |= {"info"} log.timestamp_format = "06.3f" log_file = log.to_file(prefix="tests.watchdog") stream = messaging.JsonIOStream.from_stdio(fmt("tests-{0}", tests_pid)) log.info("Spawned WatchDog-{0} for tests-{0}", tests_pid) tests_process = psutil.Process(tests_pid) stream.write_json(["watchdog", log_file.filename]) spawned_processes = {} # pid -> ProcessInfo try: stop = False while not stop: try: message = stream.read_json() except Exception: break command = message[0] args = message[1:] if command == "stop": assert not args stop = True elif command == "register_spawn": pid, name = args pid = int(pid) log.info( "WatchDog-{0} registering spawned process {1} (pid={2})", tests_pid, name, pid, ) try: _, old_name = spawned_processes[pid] except KeyError: pass else: log.warning( "WatchDog-{0} already tracks a process with pid={1}: {2}", tests_pid, pid, old_name, ) spawned_processes[pid] = ProcessInfo(psutil.Process(pid), name) elif command == "unregister_spawn": pid, name = args pid = int(pid) log.info( "WatchDog-{0} unregistering spawned process {1} (pid={2})", tests_pid, name, pid, ) spawned_processes.pop(pid, None) else: raise AssertionError( fmt("Unknown watchdog command: {0!r}", command)) stream.write_json(["ok"]) except Exception as exc: stream.write_json(["error", str(exc)]) log.reraise_exception() finally: try: stream.close() except Exception: log.swallow_exception() # If the test runner becomes a zombie process, it is still considered alive, # and wait() will block indefinitely. Poll status instead. while True: try: status = tests_process.status() except Exception: # If we can't even get its status, assume that it's dead. break # If it's dead or a zombie, time to clean it up. if status in (psutil.STATUS_DEAD, psutil.STATUS_ZOMBIE): break # Otherwise, let's wait a bit to see if anything changes. try: tests_process.wait(0.1) except Exception: pass leftover_processes = {proc for proc, _ in spawned_processes.values()} for proc, _ in spawned_processes.values(): try: leftover_processes |= proc.children(recursive=True) except Exception: pass leftover_processes = { proc for proc in leftover_processes if proc.is_running() } if not leftover_processes: return # Wait a bit to allow the terminal to catch up on the test runner output. time.sleep(0.3) log.newline(level="warning") log.warning( "tests-{0} process terminated unexpectedly, and left some orphan child " "processes behind: {1!r}", tests_pid, sorted({proc.pid for proc in leftover_processes}), ) for proc in leftover_processes: log.warning( "WatchDog-{0} killing orphaned test child process (pid={1})", tests_pid, proc.pid, ) try: proc.kill() except psutil.NoSuchProcess: pass except Exception: log.swallow_exception() log.info("WatchDog-{0} exiting", tests_pid)