Spaces:
Paused
Paused
| # Copyright (c) Microsoft Corporation. All rights reserved. | |
| # Licensed under the MIT License. See LICENSE in the project root | |
| # for license information. | |
| import itertools | |
| import os | |
| import signal | |
| import threading | |
| import time | |
| from debugpy import common | |
| from debugpy.common import log, util | |
| from debugpy.adapter import components, launchers, servers | |
| _lock = threading.RLock() | |
| _sessions = set() | |
| _sessions_changed = threading.Event() | |
| class Session(util.Observable): | |
| """A debug session involving a client, an adapter, a launcher, and a debug server. | |
| The client and the adapter are always present, and at least one of launcher and debug | |
| server is present, depending on the scenario. | |
| """ | |
| _counter = itertools.count(1) | |
| def __init__(self): | |
| from debugpy.adapter import clients | |
| super().__init__() | |
| self.lock = threading.RLock() | |
| self.id = next(self._counter) | |
| self._changed_condition = threading.Condition(self.lock) | |
| self.client = components.missing(self, clients.Client) | |
| """The client component. Always present.""" | |
| self.launcher = components.missing(self, launchers.Launcher) | |
| """The launcher componet. Always present in "launch" sessions, and never | |
| present in "attach" sessions. | |
| """ | |
| self.server = components.missing(self, servers.Server) | |
| """The debug server component. Always present, unless this is a "launch" | |
| session with "noDebug". | |
| """ | |
| self.no_debug = None | |
| """Whether this is a "noDebug" session.""" | |
| self.pid = None | |
| """Process ID of the debuggee process.""" | |
| self.debug_options = {} | |
| """Debug options as specified by "launch" or "attach" request.""" | |
| self.is_finalizing = False | |
| """Whether finalize() has been invoked.""" | |
| self.observers += [lambda *_: self.notify_changed()] | |
| def __str__(self): | |
| return f"Session[{self.id}]" | |
| def __enter__(self): | |
| """Lock the session for exclusive access.""" | |
| self.lock.acquire() | |
| return self | |
| def __exit__(self, exc_type, exc_value, exc_tb): | |
| """Unlock the session.""" | |
| self.lock.release() | |
| def register(self): | |
| with _lock: | |
| _sessions.add(self) | |
| _sessions_changed.set() | |
| def notify_changed(self): | |
| with self: | |
| self._changed_condition.notify_all() | |
| # A session is considered ended once all components disconnect, and there | |
| # are no further incoming messages from anything to handle. | |
| components = self.client, self.launcher, self.server | |
| if all(not com or not com.is_connected for com in components): | |
| with _lock: | |
| if self in _sessions: | |
| log.info("{0} has ended.", self) | |
| _sessions.remove(self) | |
| _sessions_changed.set() | |
| def wait_for(self, predicate, timeout=None): | |
| """Waits until predicate() becomes true. | |
| The predicate is invoked with the session locked. If satisfied, the method | |
| returns immediately. Otherwise, the lock is released (even if it was held | |
| at entry), and the method blocks waiting for some attribute of either self, | |
| self.client, self.server, or self.launcher to change. On every change, session | |
| is re-locked and predicate is re-evaluated, until it is satisfied. | |
| While the session is unlocked, message handlers for components other than | |
| the one that is waiting can run, but message handlers for that one are still | |
| blocked. | |
| If timeout is not None, the method will unblock and return after that many | |
| seconds regardless of whether the predicate was satisfied. The method returns | |
| False if it timed out, and True otherwise. | |
| """ | |
| def wait_for_timeout(): | |
| time.sleep(timeout) | |
| wait_for_timeout.timed_out = True | |
| self.notify_changed() | |
| wait_for_timeout.timed_out = False | |
| if timeout is not None: | |
| thread = threading.Thread( | |
| target=wait_for_timeout, name="Session.wait_for() timeout" | |
| ) | |
| thread.daemon = True | |
| thread.start() | |
| with self: | |
| while not predicate(): | |
| if wait_for_timeout.timed_out: | |
| return False | |
| self._changed_condition.wait() | |
| return True | |
| 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 _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, unless it is being replaced in situ - | |
| # so let's wait for that first. | |
| if self.server and not self.server.connection.process_replaced: | |
| log.info('{0} waiting for "exited" event...', self) | |
| if not self.wait_for( | |
| lambda: self.launcher.exit_code is not None, | |
| timeout=common.PROCESS_EXIT_TIMEOUT, | |
| ): | |
| 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 - unless the | |
| # process is being replaced in situ. | |
| if not (self.server and self.server.connection.process_replaced): | |
| 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. In addition, if the process was | |
| # replaced in situ, the launcher might still have more output to capture | |
| # from its replacement. | |
| 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. | |
| body = {} | |
| if self.client.restart_requested: | |
| body["restart"] = True | |
| try: | |
| self.client.channel.send_event("terminated", body) | |
| except Exception: | |
| pass | |
| if ( | |
| self.client.start_request is not None | |
| and self.client.start_request.command == "launch" | |
| and not (self.server and self.server.connection.process_replaced) | |
| ): | |
| 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 get(pid): | |
| with _lock: | |
| return next((session for session in _sessions if session.pid == pid), None) | |
| def wait_until_ended(): | |
| """Blocks until all sessions have ended. | |
| A session ends when all components that it manages disconnect from it. | |
| """ | |
| while True: | |
| with _lock: | |
| if not len(_sessions): | |
| return | |
| _sessions_changed.clear() | |
| _sessions_changed.wait() | |
| def report_sockets(): | |
| if not _sessions: | |
| return | |
| session = sorted(_sessions, key=lambda session: session.id)[0] | |
| client = session.client | |
| if client is not None: | |
| client.report_sockets() | |