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 run_module(): # Add current directory to path, like Python itself does for -m. This must # be in place before trying to use find_spec below to resolve submodules. sys.path.insert(0, "") # We want to do the same thing that run_module() would do here, without # actually invoking it. On Python 3, it's exposed as a public API, but # on Python 2, we have to invoke a private function in runpy for this. # Either way, if it fails to resolve for any reason, just leave argv as is. argv_0 = sys.argv[0] try: if sys.version_info >= (3,): from importlib.util import find_spec spec = find_spec(options.target) if spec is not None: argv_0 = spec.origin else: _, _, _, argv_0 = runpy._get_module_details(options.target) except Exception: log.swallow_exception("Error determining module path for sys.argv") start_debugging(argv_0) # On Python 2, module name must be a non-Unicode string, because it ends up # a part of module's __package__, and Python will refuse to run the module # if __package__ is Unicode. target = ( compat.filename_bytes(options.target) if sys.version_info < (3,) else options.target ) log.describe_environment("Pre-launch environment:") log.info("Running module {0!r}", target) # Docs say that runpy.run_module is equivalent to -m, but it's not actually # the case for packages - -m sets __name__ to "__main__", but run_module sets # it to "pkg.__main__". This breaks everything that uses the standard pattern # __name__ == "__main__" to detect being run as a CLI app. On the other hand, # runpy._run_module_as_main is a private function that actually implements -m. try: run_module_as_main = runpy._run_module_as_main except AttributeError: log.warning("runpy._run_module_as_main is missing, falling back to run_module.") runpy.run_module(target, alter_sys=True) else: run_module_as_main(target, alter_argv=True)
def _finalize(self, why, terminate_debuggee): # If the client started a session, and then disconnected before issuing "launch" # or "attach", the main thread will be blocked waiting for the first server # connection to come in - unblock it, so that we can exit. servers.dont_wait_for_first_connection() if self.server: if self.server.is_connected: if terminate_debuggee and self.launcher and self.launcher.is_connected: # If we were specifically asked to terminate the debuggee, and we # can ask the launcher to kill it, do so instead of disconnecting # from the server to prevent debuggee from running any more code. self.launcher.terminate_debuggee() else: # Otherwise, let the server handle it the best it can. try: self.server.channel.request( "disconnect", {"terminateDebuggee": terminate_debuggee}) except Exception: pass self.server.detach_from_session() if self.launcher and self.launcher.is_connected: # If there was a server, we just disconnected from it above, which should # cause the debuggee process to exit - so let's wait for that first. if self.server: log.info('{0} waiting for "exited" event...', self) if not self.wait_for( lambda: self.launcher.exit_code is not None, timeout=5): log.warning('{0} timed out waiting for "exited" event.', self) # Terminate the debuggee process if it's still alive for any reason - # whether it's because there was no server to handle graceful shutdown, # or because the server couldn't handle it for some reason. self.launcher.terminate_debuggee() # Wait until the launcher message queue fully drains. There is no timeout # here, because the final "terminated" event will only come after reading # user input in wait-on-exit scenarios. log.info("{0} waiting for {1} to disconnect...", self, self.launcher) self.wait_for(lambda: not self.launcher.is_connected) try: self.launcher.channel.close() except Exception: log.swallow_exception() if self.client: if self.client.is_connected: # Tell the client that debugging is over, but don't close the channel until it # tells us to, via the "disconnect" request. try: self.client.channel.send_event("terminated") except Exception: pass if (self.client.start_request is not None and self.client.start_request.command == "launch"): servers.stop_serving() log.info( '"launch" session ended - killing remaining debuggee processes.' ) pids_killed = set() if self.launcher and self.launcher.pid is not None: # Already killed above. pids_killed.add(self.launcher.pid) while True: conns = [ conn for conn in servers.connections() if conn.pid not in pids_killed ] if not len(conns): break for conn in conns: log.info("Killing {0}", conn) try: os.kill(conn.pid, signal.SIGTERM) except Exception: log.swallow_exception("Failed to kill {0}", conn) pids_killed.add(conn.pid)
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)