diff --git a/.vscode/launch.json b/.vscode/launch.json index dbab8562..bba9a2f5 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,13 +5,13 @@ "version": "0.2.0", "configurations": [ { - "name": "ptvsd.adapter --debug-server", + "name": "ptvsd.adapter --port", "type": "python", "request": "launch", "console": "integratedTerminal", "consoleTitle": "ptvsd.adapter", "program": "${workspaceFolder}/src/ptvsd/adapter", - "args": ["--debug-server", "8765", "--cls"], + "args": ["--port", "8765", "--cls"], "customDebugger": true, }, diff --git a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py index 33c70405..b2275aaf 100644 --- a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py +++ b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py @@ -73,7 +73,7 @@ from _pydev_imps._pydev_saved_modules import thread from _pydev_imps._pydev_saved_modules import threading from socket import AF_INET, SOCK_STREAM, SHUT_RD, SHUT_WR, SOL_SOCKET, SO_REUSEADDR, SHUT_RDWR, IPPROTO_TCP from _pydevd_bundle.pydevd_constants import (DebugInfoHolder, get_thread_id, IS_WINDOWS, IS_JYTHON, - IS_PY2, IS_PY36_OR_GREATER, STATE_RUN, dict_keys, ASYNC_EVAL_TIMEOUT_SEC, GlobalDebuggerHolder, + IS_PY2, IS_PY36_OR_GREATER, STATE_RUN, dict_keys, ASYNC_EVAL_TIMEOUT_SEC, get_global_debugger, GetGlobalDebugger, set_global_debugger) # Keep for backward compatibility @UnusedImport from _pydev_bundle.pydev_override import overrides import weakref @@ -187,7 +187,7 @@ def run_as_pydevd_daemon_thread(func, *args, **kwargs): class ReaderThread(PyDBDaemonThread): ''' reader thread reads and dispatches commands in an infinite loop ''' - def __init__(self, sock, terminate_on_socket_close=True): + def __init__(self, sock, py_db, terminate_on_socket_close=True): assert sock is not None from _pydevd_bundle.pydevd_process_net_command_json import PyDevJsonCommandProcessor from _pydevd_bundle.pydevd_process_net_command import process_net_command @@ -195,22 +195,17 @@ class ReaderThread(PyDBDaemonThread): self._terminate_on_socket_close = terminate_on_socket_close self.sock = sock + self.py_db = py_db self._buffer = b'' self.setName("pydevd.Reader") self.process_net_command = process_net_command self.process_net_command_json = PyDevJsonCommandProcessor(self._from_json).process_net_command_json - self.global_debugger_holder = GlobalDebuggerHolder - - self._dap_messages_listeners = [] def _from_json(self, json_msg, update_ids_from_dap=False): return pydevd_base_schema.from_json(json_msg, update_ids_from_dap, on_dict_loaded=self._on_dict_loaded) - def add_dap_messages_listener(self, dap_commands_listener): - self._dap_messages_listeners.append(dap_commands_listener) - def _on_dict_loaded(self, dct): - for listener in self._dap_messages_listeners: + for listener in self.py_db.dap_messages_listeners: listener.after_receive(dct) @overrides(PyDBDaemonThread.do_kill_pydev_thread) @@ -292,7 +287,7 @@ class ReaderThread(PyDBDaemonThread): return # Finished communication. # We just received a json message, let's process it. - self.process_net_command_json(self.global_debugger_holder.global_dbg, json_contents) + self.process_net_command_json(self.py_db, json_contents) continue else: @@ -347,9 +342,10 @@ class ReaderThread(PyDBDaemonThread): class WriterThread(PyDBDaemonThread): ''' writer thread writes out the commands in an infinite loop ''' - def __init__(self, sock, terminate_on_socket_close=True): + def __init__(self, sock, py_db, terminate_on_socket_close=True): PyDBDaemonThread.__init__(self) self.sock = sock + self.py_db = py_db self._terminate_on_socket_close = terminate_on_socket_close self.setName("pydevd.Writer") self.cmdQueue = _queue.Queue() @@ -358,11 +354,6 @@ class WriterThread(PyDBDaemonThread): else: self.timeout = 0.1 - self._dap_messages_listeners = [] - - def add_dap_messages_listener(self, dap_commands_listener): - self._dap_messages_listeners.append(dap_commands_listener) - def add_command(self, cmd): ''' cmd is NetCommand ''' if not self.killReceived: # we don't take new data after everybody die @@ -401,7 +392,7 @@ class WriterThread(PyDBDaemonThread): return if cmd.as_dict is not None: - for listener in self._dap_messages_listeners: + for listener in self.py_db.dap_messages_listeners: listener.before_send(cmd.as_dict) cmd.send(self.sock) @@ -413,7 +404,7 @@ class WriterThread(PyDBDaemonThread): time.sleep(self.timeout) except Exception: if self._terminate_on_socket_close: - GlobalDebuggerHolder.global_dbg.finish_debugging_session() + self.py_db.finish_debugging_session() if DebugInfoHolder.DEBUG_TRACE_LEVEL > 0: pydev_log_exception() diff --git a/src/ptvsd/_vendored/pydevd/pydevd.py b/src/ptvsd/_vendored/pydevd/pydevd.py index 59b704ca..d15a5b1e 100644 --- a/src/ptvsd/_vendored/pydevd/pydevd.py +++ b/src/ptvsd/_vendored/pydevd/pydevd.py @@ -586,6 +586,9 @@ class PyDB(object): self._apply_filter_cache = {} self._ignore_system_exit_codes = set() + # DAP related + self._dap_messages_listeners = [] + if set_as_global: # Set as the global instance only after it's initialized. set_global_debugger(self) @@ -1120,8 +1123,8 @@ class PyDB(object): if curr_writer: curr_writer.do_kill_pydev_thread() - self.writer = WriterThread(sock, terminate_on_socket_close=terminate_on_socket_close) - self.reader = ReaderThread(sock, terminate_on_socket_close=terminate_on_socket_close) + self.writer = WriterThread(sock, self, terminate_on_socket_close=terminate_on_socket_close) + self.reader = ReaderThread(sock, self, terminate_on_socket_close=terminate_on_socket_close) self.writer.start() self.reader.start() @@ -1149,6 +1152,13 @@ class PyDB(object): def wait_for_server_socket_ready(self): self._server_socket_ready_event.wait() + @property + def dap_messages_listeners(self): + return self._dap_messages_listeners + + def add_dap_messages_listener(self, listener): + self._dap_messages_listeners.append(listener) + class _WaitForConnectionThread(PyDBDaemonThread): def __init__(self, py_db): @@ -2102,16 +2112,7 @@ def add_dap_messages_listener(dap_messages_listener): if py_db is None: raise AssertionError('PyDB is still not setup.') - writer = py_db.writer - if writer is None: - raise AssertionError('PyDB.writer is still not setup.') - - reader = py_db.reader - if reader is None: - raise AssertionError('PyDB.reader is still not setup.') - - writer.add_dap_messages_listener(dap_messages_listener) - reader.add_dap_messages_listener(dap_messages_listener) + py_db.add_dap_messages_listener(dap_messages_listener) def set_debug(setup): @@ -2423,7 +2424,7 @@ def _locked_settrace( debugger.connect(host, port) # Note: connect can raise error. else: # Create a dummy writer and wait for the real connection. - debugger.writer = WriterThread(NULL, terminate_on_socket_close=False) + debugger.writer = WriterThread(NULL, debugger, terminate_on_socket_close=False) debugger.create_wait_for_connection_thread() if dont_trace_start_patterns or dont_trace_end_paterns: diff --git a/src/ptvsd/adapter/__main__.py b/src/ptvsd/adapter/__main__.py index 8f4ef9fa..687caafe 100644 --- a/src/ptvsd/adapter/__main__.py +++ b/src/ptvsd/adapter/__main__.py @@ -33,13 +33,19 @@ def main(args): log.describe_environment("ptvsd.adapter startup environment:") session = session.Session() - if args.debug_server is None: + if args.port is None: session.connect_to_ide() else: # If in debugServer mode, log everything to stderr. log.stderr_levels |= set(log.LEVELS) - with session.accept_connection_from_ide(("localhost", args.debug_server)): - pass + + if args.for_server_on_port is not None: + session.connect_to_server(("127.0.0.1", args.for_server_on_port)) + with session.accept_connection_from_ide((args.host, args.port)) as (_, port): + try: + session.server.set_debugger_property({"adapterPort": port}) + except AttributeError: + pass session.wait_for_completion() @@ -47,16 +53,29 @@ def _parse_argv(argv): parser = argparse.ArgumentParser() parser.add_argument( - "-d", - "--debug-server", + "--port", type=int, - nargs="?", default=None, - const=8765, metavar="PORT", help="start the adapter in debugServer mode on the specified port", ) + parser.add_argument( + "--host", + type=str, + default="127.0.0.1", + metavar="HOST", + help="start the adapter in debugServer mode on the specified host", + ) + + parser.add_argument( + "--for-server-on-port", + type=int, + default=None, + metavar="PORT", + help=argparse.SUPPRESS + ) + parser.add_argument( "--cls", action="store_true", help="clear screen before starting the debuggee" ) @@ -73,8 +92,8 @@ def _parse_argv(argv): ) args = parser.parse_args(argv[1:]) - if args.debug_server is None and args.log_stderr: - parser.error("--log-stderr can only be used with --debug-server") + if args.port is None and args.log_stderr: + parser.error("--log-stderr can only be used with --port") return args diff --git a/src/ptvsd/adapter/ide.py b/src/ptvsd/adapter/ide.py index d643c149..35078a43 100644 --- a/src/ptvsd/adapter/ide.py +++ b/src/ptvsd/adapter/ide.py @@ -136,7 +136,7 @@ class IDE(components.Component): assert request.is_request("launch", "attach") if self._initialize_request is None: raise request.isnt_valid("Session is not initialized yet") - if self.launcher or self.server: + if self.launcher: raise request.isnt_valid("Session is already started") self.session.no_debug = request("noDebug", json.default(False)) @@ -232,8 +232,14 @@ class IDE(components.Component): if self.session.no_debug: raise request.isnt_valid('"noDebug" is not supported for "attach"') + + pid = request("processId", int, optional=True) if pid == (): + if self.server is not None: + # we are already connected to the debug server + return + host = request("host", "127.0.0.1") port = request("port", int) if request("listen", False): @@ -242,6 +248,9 @@ class IDE(components.Component): else: self.session.connect_to_server((host, port)) else: + if self.server is not None: + raise request.isnt_valid("Session is already started") + ptvsd_args = request("ptvsdArgs", json.array(unicode)) self.session.inject_server(pid, ptvsd_args) diff --git a/src/ptvsd/adapter/server.py b/src/ptvsd/adapter/server.py index 8110ff3b..b6b0daf4 100644 --- a/src/ptvsd/adapter/server.py +++ b/src/ptvsd/adapter/server.py @@ -60,6 +60,10 @@ class Server(components.Component): request.wait_for_response() self.capabilities = self.Capabilities(self, request.response) + def set_debugger_property(self, arguments): + assert isinstance(arguments, dict) + self.channel.request("setDebuggerProperty", arguments=arguments) + # Generic request handler, used if there's no specific handler below. @message_handler def request(self, request): diff --git a/src/ptvsd/server/api.py b/src/ptvsd/server/api.py index 26b5b154..03cc0d40 100644 --- a/src/ptvsd/server/api.py +++ b/src/ptvsd/server/api.py @@ -13,10 +13,14 @@ import threading import ptvsd from ptvsd.common import log, options as common_opts from ptvsd.server import options as server_opts +from ptvsd.common.compat import queue from _pydevd_bundle.pydevd_constants import get_global_debugger from pydevd_file_utils import get_abs_path_real_path_and_base_from_file +_QUEUE_TIMEOUT = 10 + + def wait_for_attach(): log.info("wait_for_attach()") dbg = get_global_debugger() @@ -71,12 +75,61 @@ def _starts_debugging(func): @_starts_debugging def enable_attach(dont_trace_start_patterns, dont_trace_end_patterns): - server_opts.host, server_opts.port = pydevd._enable_attach( - (server_opts.host, server_opts.port), + if hasattr(enable_attach, "called"): + raise RuntimeError("'enable_attach' can only be called once per process.") + + host, port = pydevd._enable_attach( + ("127.0.0.1", 0), dont_trace_start_patterns=dont_trace_start_patterns, dont_trace_end_paterns=dont_trace_end_patterns, patch_multiprocessing=server_opts.multiprocess, ) + + log.info("pydevd debug server running at: {0}:{1}", host, port) + + port_queue = queue.Queue() + class _DAPMessagesListener(pydevd.IDAPMessagesListener): + def before_send(self, msg): + pass + + def after_receive(self, msg): + try: + if msg["command"] == "setDebuggerProperty": + port_queue.put(msg["arguments"]["adapterPort"]) + except KeyError: + pass + + pydevd.add_dap_messages_listener(_DAPMessagesListener()) + + with pydevd.skip_subprocess_arg_patch(): + import subprocess + adapter_args = [ + sys.executable, + os.path.join(os.path.dirname(ptvsd.__file__), "adapter"), + "--host", + server_opts.host, + "--port", + str(server_opts.port), + "--for-server-on-port", + str(port) + ] + + if common_opts.log_dir is not None: + adapter_args += ["--log-dir", common_opts.log_dir] + + log.info( + "enable_attach() spawning attach-to-PID debugger injector: {0!r}", adapter_args + ) + + # Adapter life time is expected to be longer than this process, + # so never wait on the adapter process + # TODO: Add adapter PID to ignore list https://github.com/microsoft/ptvsd/issues/1786 + subprocess.Popen(adapter_args, bufsize=0) + + server_opts.port = port_queue.get(True, _QUEUE_TIMEOUT) + + enable_attach.called = True + log.info("ptvsd debug server running at: {0}:{1}", server_opts.host, server_opts.port) return server_opts.host, server_opts.port