You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			294 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			294 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
# 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()
 |