| | |
| | |
| | |
| |
|
| | 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() |
| |
|
| | |
| | |
| | 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: |
| | |
| | |
| | log.swallow_exception("Fatal error while finalizing {0}", self) |
| | os._exit(1) |
| |
|
| | log.info("{0} finalized.", self) |
| |
|
| | def _finalize(self, why, terminate_debuggee): |
| | |
| | |
| | |
| | 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: |
| | |
| | |
| | |
| | self.launcher.terminate_debuggee() |
| | else: |
| | |
| | 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 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) |
| |
|
| | |
| | |
| | |
| | |
| | if not (self.server and self.server.connection.process_replaced): |
| | self.launcher.terminate_debuggee() |
| |
|
| | |
| | |
| | |
| | |
| | |
| | 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: |
| | |
| | |
| | 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: |
| | |
| | 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() |
| |
|