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