enable_attach should spawn adapter (#1784)

* Spawn adapter in enable attach.

* Addressing comments and simplifying

* Minor tweaks
This commit is contained in:
Karthik Nadig 2019-09-19 10:51:21 -07:00 committed by GitHub
parent 0825dbeb37
commit 636400a24a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 122 additions and 45 deletions

4
.vscode/launch.json vendored
View file

@ -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,
},

View file

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

View file

@ -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:

View file

@ -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

View file

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

View file

@ -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):

View file

@ -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