|
|
|
|
|
|
|
|
|
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() |
|
|