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) def option(name, type, *args): try: return type(os.environ.pop(name, *args)) except Exception: log.reraise_exception("Error parsing {0!r}:", name) launcher_port = option("DEBUGPY_LAUNCHER_PORT", int) launcher.connect(launcher_port) launcher.channel.wait() if debuggee.process is not None: sys.exit(debuggee.process.returncode)
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 ensure_logging(): """Starts logging to log.log_dir, if it hasn't already been done. """ if ensure_logging.ensured: return ensure_logging.ensured = True log.to_file(prefix="debugpy.server") log.describe_environment("Initial environment:")
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 test_wrapper(request, long_tmpdir): def write_log(filename, data): filename = os.path.join(log.log_dir, filename) if not isinstance(data, bytes): data = data.encode("utf-8") with open(filename, "wb") as f: f.write(data) session.Session.reset_counter() session.Session.tmpdir = long_tmpdir original_log_dir = log.log_dir failed = True try: if log.log_dir is None: log.log_dir = (long_tmpdir / "debugpy_logs").strpath else: log_subdir = request.node.nodeid log_subdir = log_subdir.replace("::", "/") for ch in r":?*|<>": log_subdir = log_subdir.replace(ch, fmt("&#{0};", ord(ch))) log.log_dir += "/" + log_subdir try: py.path.local(log.log_dir).remove() except Exception: pass print("\n") # make sure on-screen logs start on a new line with log.to_file(prefix="tests"): timestamp.reset() log.info("{0} started.", request.node.nodeid) try: yield finally: failed = False for report_attr in ("setup_report", "call_report", "teardown_report"): try: report = getattr(request.node, report_attr) except AttributeError: continue failed |= report.failed log.write_format( "error" if report.failed else "info", "pytest {0} phase for {1} {2}.", report.when, request.node.nodeid, report.outcome, ) write_log(report_attr + ".log", report.longreprtext) write_log(report_attr + ".stdout.log", report.capstdout) write_log(report_attr + ".stderr.log", report.capstderr) if failed: write_log("FAILED.log", "") logs.dump() finally: if not failed and not request.config.option.debugpy_log_passed: try: py.path.local(log.log_dir).remove() except Exception: pass log.log_dir = original_log_dir
from debugpy.common import json, log import debugpy.server # noqa # Clean up environment variables that were automatically set when importing pydevd - # we don't need them in the test runner process (since pydevd is not tracing it), # and some tests must be able to spawn debuggee with them unset. for name in ( "DEBUGPY_LOG_DIR", "PYDEVD_DEBUG", "PYDEVD_DEBUG_FILE", "PYDEVD_USE_FRAME_EVAL", ): os.environ.pop(name, None) # Enable full logging to stderr, and make timestamps shorter to match maximum test # run time better. log.stderr.levels = all log.timestamp_format = "06.3f" log.to_file(prefix="tests") # Enable JSON serialization for py.path.local. def json_default(self, obj): if isinstance(obj, py.path.local): return obj.strpath return self.original_default(obj) json.JsonEncoder.original_default = json.JsonEncoder.default json.JsonEncoder.default = json_default
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)