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["name"] = fmt("Subprocess {0}", conn.pid) body["request"] = "attach" if "host" not in body: body["host"] = "127.0.0.1" if "port" not in body: _, body["port"] = self.listener.getsockname() if "processId" in body: del body["processId"] body["subProcessId"] = conn.pid self.channel.send_event("ptvsd_attach", body)
def wait_until_port_is_listening(port, interval=1, max_attempts=1000): """Blocks until the specified TCP port on localhost is listening, and can be connected to. Tries to connect to the port periodically, and repeats until connection succeeds. Connection is immediately closed before returning. """ for i in compat.xrange(1, max_attempts + 1): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: log.info("Probing localhost:{0} (attempt {1})...", port, i) sock.connect(("localhost", port)) except socket.error as exc: # The first attempt will almost always fail, because the port isn't # open yet. But if it keeps failing after that, we want to know why. if i > 1: log.warning("Failed to connect to localhost:{0}:\n{1}", port, exc) time.sleep(interval) else: log.info("localhost:{0} is listening - server is up!", port) return finally: sock.close()
def _finalize(self, why, terminate_debuggee): # If the IDE 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.exception() # Tell the IDE that debugging is over, but don't close the channel until it # tells us to, via the "disconnect" request. if self.ide and self.ide.is_connected: try: self.ide.channel.send_event("terminated") except Exception: pass
def _finalize(self, why, terminate_debuggee): if self.server and self.server.is_connected: try: self.server.channel.request( "disconnect", {"terminateDebuggee": terminate_debuggee} ) except Exception: pass try: self.server.channel.close() except Exception: log.exception() # Wait until the server message queue fully drains - there won't be any # more events after close(), but there may still be pending responses. log.info("{0} waiting for {1} to disconnect...", self, self.server) if not self.wait_for(lambda: not self.server.is_connected, timeout=5): log.warning( "{0} timed out waiting for {1} to disconnect.", self, self.server ) 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. try: self.launcher.channel.request("terminate") except Exception: pass # 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.exception() # Tell the IDE that debugging is over, but don't close the channel until it # tells us to, via the "disconnect" request. if self.ide and self.ide.is_connected: try: self.ide.channel.send_event("terminated") except Exception: pass
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.exception("Error determining module path for sys.argv") setup_debug_server(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 __init__(self, session, connection): assert connection.server is None with session: assert not session.server super(Server, self).__init__(session, channel=connection.channel) self.connection = connection assert self.session.pid is None if self.session.launcher and self.session.launcher.pid != self.pid: log.warning( "Launcher reported PID={0}, but server reported PID={1}", self.session.pid, self.pid, ) self.session.pid = self.pid session.server = self
def __init__(self, category, fd, tee_fd, encoding): assert category not in self.instances self.instances[category] = self log.info("Capturing {0} of {1}.", category, debuggee.describe()) self.category = category self._fd = fd self._tee_fd = tee_fd try: self._decoder = codecs.getincrementaldecoder(encoding)( errors="replace") except LookupError: self._decoder = None log.warning( 'Unable to generate "output" events for {0} - unknown encoding {1!r}', category, encoding, ) self._worker_thread = threading.Thread(target=self._worker, name=category) self._worker_thread.start()
def main(tests_pid): # To import ptvsd, the "" entry in sys.path - which is added automatically on # Python 2 - must be removed first; otherwise, we end up importing tests/ptvsd. if "" in sys.path: sys.path.remove("") from ptvsd.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 ex: stream.write_json(["error", str(ex)]) raise log.exception() finally: try: stream.close() except Exception: log.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, ) if platform.system() == "Linux": try: # gcore will automatically add pid to the filename core_file = os.path.join(tempfile.gettempdir(), "ptvsd_core") gcore_cmd = fmt("gcore -o {0} {1}", core_file, proc.pid) log.warning("WatchDog-{0}: {1}", tests_pid, gcore_cmd) os.system(gcore_cmd) except Exception: log.exception() try: proc.kill() except psutil.NoSuchProcess: pass except Exception: log.exception() log.info("WatchDog-{0} exiting", tests_pid)