diff --git a/ptvsd/daemon.py b/ptvsd/daemon.py index ee592f06..e58b41cf 100644 --- a/ptvsd/daemon.py +++ b/ptvsd/daemon.py @@ -8,7 +8,7 @@ from ptvsd.socket import ( from .exit_handlers import ( ExitHandlers, UnsupportedSignalError, kill_current_proc) -from .session import DebugSession +from .session import PyDevdDebugSession from ._util import ignore_errors, debug @@ -49,8 +49,10 @@ class DaemonStoppedError(DaemonError): # TODO: Inherit from Closeable. # TODO: Inherit from Startable? -class Daemon(object): - """The process-level manager for the VSC protocol debug adapter.""" +class DaemonBase(object): + """The base class for DAP daemons.""" + + SESSION = None exitcode = 0 @@ -61,13 +63,15 @@ class Daemon(object): self._started = False self._closed = False - self._pydevd = None # set when started + # socket-related + + self._sock = None # set when started + self._server = None # session-related self._singlesession = singlesession - self._server = None self._session = None self._numsessions = 0 self._sessionlock = None @@ -84,10 +88,6 @@ class Daemon(object): if addhandlers: self._install_exit_handlers() - @property - def pydevd(self): - return self._pydevd - @property def session(self): """The current session.""" @@ -118,7 +118,9 @@ class Daemon(object): def is_running(self): """Return True if the daemon is running.""" - if self._pydevd is None: + if self._closed: + return False + if self._sock is None: return False return True @@ -130,27 +132,29 @@ class Daemon(object): raise RuntimeError('already started') self._started = True - return self._start() + sock = self._start() + self._sock = sock + return sock def start_server(self, addr, hidebadsessions=True): - """Return (pydevd "socket", next_session) with a new server socket.""" + """Return ("socket", next_session) with a new server socket.""" addr = Address.from_raw(addr) with self.started(): assert self._sessionlock is None assert self.session is None self._server = create_server(addr.host, addr.port) self._sessionlock = threading.Lock() - pydevd = self._pydevd + sock = self._sock - def check_ready(): - self._check_ready_for_session() + def check_ready(**kwargs): + self._check_ready_for_session(**kwargs) if self._server is None: raise DaemonStoppedError() - def next_session(**kwargs): + def next_session(timeout=None, **kwargs): server = self._server sessionlock = self._sessionlock - check_ready() + check_ready(checksession=False) debug('getting next session') sessionlock.acquire() # Released in _finish_session(). @@ -164,7 +168,7 @@ class Daemon(object): client = connect(server, None, **kwargs) self._bind_session(client) debug('starting session') - self._start_session('ptvsd.Server', timeout) + self._start_session_safely('ptvsd.Server', timeout=timeout) debug('session started') return self._session except Exception as exc: @@ -178,18 +182,18 @@ class Daemon(object): self._stop_quietly() raise - return pydevd, next_session + return sock, next_session def start_client(self, addr): - """Return (pydevd "socket", start_session) with a new client socket.""" + """Return ("socket", start_session) with a new client socket.""" addr = Address.from_raw(addr) with self.started(): assert self.session is None client = create_client() connect(client, addr) - pydevd = self._pydevd + sock = self._sock - def start_session(): + def start_session(**kwargs): self._check_ready_for_session() if self._server is not None: raise RuntimeError('running as server') @@ -198,15 +202,15 @@ class Daemon(object): try: self._bind_session(client) - self._start_session('ptvsd.Client', None) + self._start_session_safely('ptvsd.Client', **kwargs) return self._session except Exception: self._stop_quietly() raise - return pydevd, start_session + return sock, start_session - def start_session(self, session, threadname, timeout=None): + def start_session(self, session, threadname, **kwargs): """Start the debug session and remember it. If "session" is a client socket then a session is created @@ -217,7 +221,7 @@ class Daemon(object): raise RuntimeError('running as server') self._bind_session(session) - self._start_session(threadname, timeout) + self._start_session_safely(threadname, **kwargs) return self.session def close(self): @@ -228,41 +232,26 @@ class Daemon(object): self._close() - def re_build_breakpoints(self): - """Restore the breakpoints to their last values.""" - if self.session is None: - return - return self.session.re_build_breakpoints() - # internal methods - def _check_ready_for_session(self): + def _check_ready_for_session(self, checksession=True): if self._closed: raise DaemonClosedError() if not self._started: raise DaemonStoppedError('never started') - if self._pydevd is None: + if self._sock is None: raise DaemonStoppedError() - if self.session is not None: + if checksession and self.session is not None: raise RuntimeError('session already started') def _close(self): self._stop() - self._pydevd = None + self._sock = None if self._wait_on_exit(self.exitcode): self._wait_for_user() - def _start(self): - self._pydevd = wrapper.PydevdSocket( - self._handle_pydevd_message, - self._handle_pydevd_close, - self._getpeername, - self._getsockname, - ) - return self._pydevd - def _stop(self): sessionlock = self._sessionlock self._sessionlock = None @@ -282,9 +271,9 @@ class Daemon(object): with ignore_errors(): close_socket(server) - if self._pydevd is not None: + if self._sock is not None: with ignore_errors(): - close_socket(self._pydevd) + close_socket(self._sock) def _stop_quietly(self): if self._closed: # XXX wrong? @@ -307,7 +296,7 @@ class Daemon(object): # internal session-related methods def _bind_session(self, session): - session = DebugSession.from_raw( + session = self.SESSION.from_raw( session, notify_closing=self._handle_session_closing, ownsock=True, @@ -315,14 +304,9 @@ class Daemon(object): self._session = session self._numsessions += 1 - def _start_session(self, threadname, timeout): + def _start_session_safely(self, threadname, **kwargs): try: - self.session.start( - threadname, - self._pydevd.pydevd_notify, - self._pydevd.pydevd_request, - timeout=timeout, - ) + self._start_session(threadname, **kwargs) except Exception: with ignore_errors(): self._finish_session() @@ -398,6 +382,52 @@ class Daemon(object): if not self._exiting_via_atexit_handler: sys.exit(0) + # methods for subclasses to override + + def _start(self): + """Return the debugger client socket after starting the daemon.""" + raise NotImplementedError + + def _start_session(self, threadname, **kwargs): + self.session.start( + threadname, + **kwargs + ) + + +class Daemon(DaemonBase): + """The process-level manager for the VSC protocol debug adapter.""" + + SESSION = PyDevdDebugSession + + @property + def pydevd(self): + return self._sock + + def re_build_breakpoints(self): + """Restore the breakpoints to their last values.""" + if self.session is None: + return + return self.session.re_build_breakpoints() + + # internal methods + + def _start(self): + return wrapper.PydevdSocket( + self._handle_pydevd_message, + self._handle_pydevd_close, + self._getpeername, + self._getsockname, + ) + + def _start_session(self, threadname, **kwargs): + super(Daemon, self)._start_session( + threadname, + pydevd_notify=self.pydevd.pydevd_notify, + pydevd_request=self.pydevd.pydevd_request, + **kwargs + ) + # internal methods for PyDevdSocket(). def _handle_pydevd_message(self, cmdid, seq, text): diff --git a/ptvsd/pydevd_hooks.py b/ptvsd/pydevd_hooks.py index fff5bd79..014fc97f 100644 --- a/ptvsd/pydevd_hooks.py +++ b/ptvsd/pydevd_hooks.py @@ -8,7 +8,7 @@ from ptvsd.daemon import Daemon, DaemonStoppedError, DaemonClosedError from ptvsd._util import debug -def start_server(daemon, host, port): +def start_server(daemon, host, port, **kwargs): """Return a socket to a (new) local pydevd-handling daemon. The daemon supports the pydevd client wire protocol, sending @@ -16,11 +16,11 @@ def start_server(daemon, host, port): This is a replacement for _pydevd_bundle.pydevd_comm.start_server. """ - pydevd, next_session = daemon.start_server((host, port)) + sock, next_session = daemon.start_server((host, port)) def handle_next(): try: - session = next_session() + session = next_session(**kwargs) debug('done waiting') return session except (DaemonClosedError, DaemonStoppedError): @@ -54,10 +54,10 @@ def start_server(daemon, host, port): t.is_pydev_daemon_thread = True t.daemon = True t.start() - return pydevd + return sock -def start_client(daemon, host, port): +def start_client(daemon, host, port, **kwargs): """Return a socket to an existing "remote" pydevd-handling daemon. The daemon supports the pydevd client wire protocol, sending @@ -65,9 +65,9 @@ def start_client(daemon, host, port): This is a replacement for _pydevd_bundle.pydevd_comm.start_client. """ - pydevd, start_session = daemon.start_client((host, port)) - start_session() - return pydevd + sock, start_session = daemon.start_client((host, port)) + start_session(**kwargs) + return sock def install(pydevd, address, diff --git a/ptvsd/runner.py b/ptvsd/runner.py index d4180b8e..9345598a 100644 --- a/ptvsd/runner.py +++ b/ptvsd/runner.py @@ -2,33 +2,28 @@ # Licensed under the MIT License. See LICENSE in the project root # for license information. -import atexit -import os -import platform import pydevd -import signal -import socket import sys import time import threading import traceback -import warnings -from ptvsd import ipcjson, __version__ -from ptvsd.daemon import DaemonClosedError -from ptvsd.pydevd_hooks import start_client -from ptvsd.socket import close_socket -from ptvsd.wrapper import WAIT_FOR_DISCONNECT_REQUEST_TIMEOUT, WAIT_FOR_THREAD_FINISH_TIMEOUT, INITIALIZE_RESPONSE # noqa +from ptvsd.daemon import DaemonBase +from ptvsd.session import DebugSession +from ptvsd.wrapper import ( + WAIT_FOR_THREAD_FINISH_TIMEOUT, VSCLifecycleMsgProcessor) from pydevd import init_stdout_redirect, init_stderr_redirect + HOSTNAME = 'localhost' -WAIT_FOR_LAUNCH_REQUEST_TIMEOUT = 10000 OUTPUT_POLL_PERIOD = 0.3 def run(address, filename, is_module, *args, **kwargs): + # TODO: docstring # TODO: client/server -> address - if not start_message_processor(*address): + daemon = Daemon() + if not daemon.wait_for_launch(address): return debugger = pydevd.PyDB() @@ -45,18 +40,8 @@ def run(address, filename, is_module, *args, **kwargs): time.sleep(OUTPUT_POLL_PERIOD + 0.1) -def start_message_processor(host, port_num): - launch_notification = threading.Event() - - daemon = Daemon( - notify_launch=launch_notification.set, - addhandlers=True, killonclose=True) - start_client(daemon, host, port_num) - - return launch_notification.wait(WAIT_FOR_LAUNCH_REQUEST_TIMEOUT) - - class OutputRedirection(object): + # TODO: docstring def __init__(self, on_output=lambda category, output: None): self._on_output = on_output @@ -64,6 +49,7 @@ class OutputRedirection(object): self._thread = None def start(self): + # TODO: docstring init_stdout_redirect() init_stderr_redirect() self._thread = threading.Thread( @@ -74,6 +60,7 @@ class OutputRedirection(object): self._thread.start() def stop(self): + # TODO: docstring if self._stopped: return @@ -81,7 +68,6 @@ class OutputRedirection(object): self._thread.join(WAIT_FOR_THREAD_FINISH_TIMEOUT) def _run(self): - import sys while not self._stopped: self._check_output(sys.stdoutBuf, 'stdout') self._check_output(sys.stderrBuf, 'stderr') @@ -104,243 +90,47 @@ class OutputRedirection(object): traceback.print_exc() -# TODO: Inherit from ptvsd.daemon.Daemon. - -class Daemon(object): +class Daemon(DaemonBase): """The process-level manager for the VSC protocol debug adapter.""" - def __init__(self, - notify_launch=lambda: None, - addhandlers=True, - killonclose=True): + LAUNCH_TIMEOUT = 10000 # seconds - self.exitcode = 0 - self.exiting_via_exit_handler = False + class SESSION(DebugSession): + class MESSAGE_PROCESSOR(VSCLifecycleMsgProcessor): + def on_invalid_request(self, request, args): + self.send_response(request, success=True) - self.addhandlers = addhandlers - self.killonclose = killonclose - self._notify_launch = notify_launch - - self._closed = False - self._client = None - self._adapter = None - - def start(self): - if self._closed: - raise DaemonClosedError() + def wait_for_launch(self, addr, timeout=LAUNCH_TIMEOUT): + # TODO: docstring + launched = threading.Event() + _, start_session = self.start_client(addr) + start_session( + notify_launch=launched.set, + ) + return launched.wait(timeout) + def _start(self): self._output_monitor = OutputRedirection(self._send_output) self._output_monitor.start() + return NoSocket() - return None - - def start_session(self, client): - """Set the client socket to use for the debug adapter. - - A VSC message loop is started for the client. - """ - if self._closed: - raise DaemonClosedError() - if self._client is not None: - raise RuntimeError('connection already set') - self._client = client - - self._adapter = VSCodeMessageProcessor( - client, - self._notify_launch, - self._handle_vsc_disconnect, - self._handle_vsc_close, - ) - self._adapter.start() - if self.addhandlers: - self._add_atexit_handler() - self._set_signal_handlers() - return self._adapter - - def close(self): - """Stop all loops and release all resources.""" + def _close(self): self._output_monitor.stop() - if self._closed: - raise DaemonClosedError('already closed') - self._closed = True - - if self._client is not None: - self._release_connection() - - # internal methods - - def _add_atexit_handler(self): - - def handler(): - self.exiting_via_exit_handler = True - if not self._closed: - self.close() - if self._adapter is not None: - self._adapter._wait_for_server_thread() - - atexit.register(handler) - - def _set_signal_handlers(self): - if platform.system() == 'Windows': - return None - - def handler(signum, frame): - if not self._closed: - self.close() - sys.exit(0) - - signal.signal(signal.SIGHUP, handler) - - def _release_connection(self): - if self._adapter is not None: - self._adapter.handle_stopped(self.exitcode) - self._adapter.close() - close_socket(self._client) - - # internal methods for VSCodeMessageProcessor - - def _handle_vsc_disconnect(self, kill=False): - if not self._closed: - self.close() - if kill and self.killonclose and not self.exiting_via_exit_handler: - os.kill(os.getpid(), signal.SIGTERM) - - def _handle_vsc_close(self): - if self._closed: - return - self.close() + super(Daemon, self)._close() def _send_output(self, category, output): - self._adapter.send_event('output', category=category, output=output) + if self.session is None: + return + self.session._msgprocessor.send_event('output', + category=category, + output=output) -class VSCodeMessageProcessor(ipcjson.SocketIO, ipcjson.IpcChannel): - """IPC JSON message processor for VSC debugger protocol. +class NoSocket(object): + """A object with a noop socket lifecycle.""" - This translates between the VSC debugger protocol and the pydevd - protocol. - """ - - def __init__( - self, - socket, - notify_launch=lambda: None, - notify_disconnecting=lambda: None, - notify_closing=lambda: None, - logfile=None, - ): - super(VSCodeMessageProcessor, self).__init__( - socket=socket, own_socket=False, logfile=logfile) - self._socket = socket - self._notify_launch = notify_launch - self._notify_disconnecting = notify_disconnecting - self._notify_closing = notify_closing - - self.server_thread = None - self._closed = False - - # adapter state - self.disconnect_request = None - self.disconnect_request_event = threading.Event() - self._exited = False - - def start(self): - # VSC msg processing loop - self.server_thread = threading.Thread( - target=self.process_messages, - name='ptvsd.Client', - ) - self.server_thread.pydev_do_not_trace = True - self.server_thread.is_pydev_daemon_thread = True - self.server_thread.daemon = True - self.server_thread.start() - - # special initialization - self.send_event( - 'output', - category='telemetry', - output='ptvsd', - data={ - 'version': __version__, - 'nodebug': True - }, - ) - - # closing the adapter + def shutdown(self, *args, **kwargs): + pass def close(self): - """Stop the message processor and release its resources.""" - if self._closed: - return - self._closed = True - - self._notify_closing() - # Close the editor-side socket. - self._stop_vsc_message_loop() - - def _stop_vsc_message_loop(self): - self.set_exit() - if self._socket: - try: - self._socket.shutdown(socket.SHUT_RDWR) - self._socket.close() - except Exception: - pass - - def _wait_for_server_thread(self): - if self.server_thread is None: - return - if not self.server_thread.is_alive(): - return - self.server_thread.join(WAIT_FOR_THREAD_FINISH_TIMEOUT) - - def handle_stopped(self, exitcode): - """Finalize the protocol connection.""" - if self._exited: - return - self._exited = True - - # Notify the editor that the "debuggee" (e.g. script, app) exited. - self.send_event('exited', exitCode=exitcode) - - # Notify the editor that the debugger has stopped. - self.send_event('terminated') - - # The editor will send a "disconnect" request at this point. - self._wait_for_disconnect() - - def _wait_for_disconnect(self, timeout=None): - if timeout is None: - timeout = WAIT_FOR_DISCONNECT_REQUEST_TIMEOUT - - if not self.disconnect_request_event.wait(timeout): - warnings.warn('timed out waiting for disconnect request') - if self.disconnect_request is not None: - self.send_response(self.disconnect_request) - self.disconnect_request = None - - def _handle_disconnect(self, request): - self.disconnect_request = request - self.disconnect_request_event.set() - self._notify_disconnecting(not self._closed) - if not self._closed: - self.close() - - # VSC protocol handlers - - def on_initialize(self, request, args): - self.send_response(request, **INITIALIZE_RESPONSE) - self.send_event('initialized') - - def on_configurationDone(self, request, args): - self.send_response(request) - - def on_launch(self, request, args): - self._notify_launch() - self.send_response(request) - - def on_disconnect(self, request, args): - self._handle_disconnect(request) - - def on_invalid_request(self, request, args): - self.send_response(request, success=True) + pass diff --git a/ptvsd/session.py b/ptvsd/session.py index 0112f298..ef7f9611 100644 --- a/ptvsd/session.py +++ b/ptvsd/session.py @@ -6,6 +6,8 @@ from ._util import Closeable, Startable, debug class DebugSession(Startable, Closeable): """A single DAP session for a network client socket.""" + MESSAGE_PROCESSOR = None + NAME = 'debug session' FAIL_ON_ALREADY_CLOSED = False FAIL_ON_ALREADY_STOPPED = False @@ -57,18 +59,6 @@ class DebugSession(Startable, Closeable): def msgprocessor(self): return self._msgprocessor - def handle_pydevd_message(self, cmdid, seq, text): - if self._msgprocessor is None: - # TODO: Do more than ignore? - return - return self._msgprocessor.on_pydevd_event(cmdid, seq, text) - - def re_build_breakpoints(self): - """Restore the breakpoints to their last values.""" - if self._msgprocessor is None: - return - return self._msgprocessor.re_build_breakpoints() - def wait_options(self): """Return (normal, abnormal) based on the session's launch config.""" if self._msgprocessor is None: @@ -96,19 +86,17 @@ class DebugSession(Startable, Closeable): # internal methods - def _start(self, threadname, pydevd_notify, pydevd_request, timeout=None): - """Start the message handling for the session. - - A VSC message loop is started. - """ - self._msgprocessor = VSCodeMessageProcessor( + def _new_msg_processor(self, **kwargs): + return self.MESSAGE_PROCESSOR( self._sock, - pydevd_notify, - pydevd_request, notify_disconnecting=self._handle_vsc_disconnect, notify_closing=self._handle_vsc_close, - timeout=timeout, + **kwargs ) + + def _start(self, threadname, **kwargs): + """Start the message handling for the session.""" + self._msgprocessor = self._new_msg_processor(**kwargs) self.add_resource_to_close(self._msgprocessor) self._msgprocessor.start(threadname) return self._msgprocessor_running @@ -142,3 +130,21 @@ class DebugSession(Startable, Closeable): def _handle_vsc_close(self): debug('processor closing') self.close() + + +class PyDevdDebugSession(DebugSession): + """A single DAP session for a network client socket.""" + + MESSAGE_PROCESSOR = VSCodeMessageProcessor + + def handle_pydevd_message(self, cmdid, seq, text): + if self._msgprocessor is None: + # TODO: Do more than ignore? + return + return self._msgprocessor.on_pydevd_event(cmdid, seq, text) + + def re_build_breakpoints(self): + """Restore the breakpoints to their last values.""" + if self._msgprocessor is None: + return + return self._msgprocessor.re_build_breakpoints() diff --git a/ptvsd/wrapper.py b/ptvsd/wrapper.py index bdf6bdec..df197a07 100644 --- a/ptvsd/wrapper.py +++ b/ptvsd/wrapper.py @@ -48,35 +48,19 @@ from ptvsd.socket import TimeoutError # noqa # print(s) #ipcjson._TRACE = ipcjson_trace + WAIT_FOR_DISCONNECT_REQUEST_TIMEOUT = 2 WAIT_FOR_THREAD_FINISH_TIMEOUT = 1 -INITIALIZE_RESPONSE = dict( - supportsExceptionInfoRequest=True, - supportsConfigurationDoneRequest=True, - supportsConditionalBreakpoints=True, - supportsHitConditionalBreakpoints=True, - supportsSetVariable=True, - supportsExceptionOptions=True, - supportsEvaluateForHovers=True, - supportsValueFormattingOptions=True, - supportsSetExpression=True, - supportsModulesRequest=True, - supportsLogPoints=True, - supportTerminateDebuggee=True, - exceptionBreakpointFilters=[ - { - 'filter': 'raised', - 'label': 'Raised Exceptions', - 'default': False - }, - { - 'filter': 'uncaught', - 'label': 'Uncaught Exceptions', - 'default': True - }, - ], -) + +def is_debugger_internal_thread(thread_name): + # TODO: docstring + if thread_name: + if thread_name.startswith('pydevd.'): + return True + elif thread_name.startswith('ptvsd.'): + return True + return False class SafeReprPresentationProvider(pydevd_extapi.StrPresentationProvider): @@ -702,72 +686,137 @@ class InternalsFilter(object): return False -class VSCodeMessageProcessor(ipcjson.SocketIO, ipcjson.IpcChannel): - """IPC JSON message processor for VSC debugger protocol. +######################## +# the debug config - This translates between the VSC debugger protocol and the pydevd - protocol. +def bool_parser(str): + return str in ("True", "true", "1") + + +DEBUG_OPTIONS_PARSER = { + 'WAIT_ON_ABNORMAL_EXIT': bool_parser, + 'WAIT_ON_NORMAL_EXIT': bool_parser, + 'REDIRECT_OUTPUT': bool_parser, + 'VERSION': unquote, + 'INTERPRETER_OPTIONS': unquote, + 'WEB_BROWSER_URL': unquote, + 'DJANGO_DEBUG': bool_parser, + 'FLASK_DEBUG': bool_parser, + 'FIX_FILE_PATH_CASE': bool_parser, + 'WINDOWS_CLIENT': bool_parser, + 'DEBUG_STDLIB': bool_parser, +} + + +DEBUG_OPTIONS_BY_FLAG = { + 'RedirectOutput': 'REDIRECT_OUTPUT=True', + 'WaitOnNormalExit': 'WAIT_ON_NORMAL_EXIT=True', + 'WaitOnAbnormalExit': 'WAIT_ON_ABNORMAL_EXIT=True', + 'Django': 'DJANGO_DEBUG=True', + 'Flask': 'FLASK_DEBUG=True', + 'Jinja': 'FLASK_DEBUG=True', + 'FixFilePathCase': 'FIX_FILE_PATH_CASE=True', + 'DebugStdLib': 'DEBUG_STDLIB=True', + 'WindowsClient': 'WINDOWS_CLIENT=True', +} + + +def _extract_debug_options(opts, flags=None): + """Return the debug options encoded in the given value. + + "opts" is a semicolon-separated string of "key=value" pairs. + "flags" is a list of strings. + + If flags is provided then it is used as a fallback. + + The values come from the launch config: + + { + type:'python', + request:'launch'|'attach', + name:'friendly name for debug config', + debugOptions:[ + 'RedirectOutput', 'Django' + ], + options:'REDIRECT_OUTPUT=True;DJANGO_DEBUG=True' + } + + Further information can be found here: + + https://code.visualstudio.com/docs/editor/debugging#_launchjson-attributes """ + if not opts: + opts = _build_debug_options(flags) + return _parse_debug_options(opts) - def __init__(self, socket, pydevd_notify, pydevd_request, - notify_disconnecting, notify_closing, + +def _build_debug_options(flags): + """Build string representation of debug options from the launch config.""" + return ';'.join(DEBUG_OPTIONS_BY_FLAG[flag] + for flag in flags or [] + if flag in DEBUG_OPTIONS_BY_FLAG) + + +def _parse_debug_options(opts): + """Debug options are semicolon separated key=value pairs + WAIT_ON_ABNORMAL_EXIT=True|False + WAIT_ON_NORMAL_EXIT=True|False + REDIRECT_OUTPUT=True|False + VERSION=string + INTERPRETER_OPTIONS=string + WEB_BROWSER_URL=string url + DJANGO_DEBUG=True|False + WINDOWS_CLIENT=True|False + DEBUG_STDLIB=True|False + """ + options = {} + if not opts: + return options + + for opt in opts.split(';'): + try: + key, value = opt.split('=') + except ValueError: + continue + try: + options[key] = DEBUG_OPTIONS_PARSER[key](value) + except KeyError: + continue + + if 'WINDOWS_CLIENT' not in options: + options['WINDOWS_CLIENT'] = platform.system() == 'Windows' # noqa + + return options + + +######################## +# the message processor + +# TODO: Embed instead of extend (inheritance -> composition). + +class VSCodeMessageProcessorBase(ipcjson.SocketIO, ipcjson.IpcChannel): + """The base class for VSC message processors.""" + + def __init__(self, socket, notify_closing, timeout=None, logfile=None, ): - super(VSCodeMessageProcessor, self).__init__(socket=socket, - own_socket=False, - timeout=timeout, - logfile=logfile) + super(VSCodeMessageProcessorBase, self).__init__( + socket=socket, + own_socket=False, + timeout=timeout, + logfile=logfile, + ) self.socket = socket - self._pydevd_notify = pydevd_notify - self._pydevd_request = pydevd_request - self._notify_disconnecting = notify_disconnecting self._notify_closing = notify_closing - self.loop = None - self.event_loop_thread = None self.server_thread = None self._closed = False - self.bkpoints = None - - # debugger state - self.is_process_created = False - self.is_process_created_lock = threading.Lock() - self.stack_traces = {} - self.stack_traces_lock = threading.Lock() - self.active_exceptions = {} - self.active_exceptions_lock = threading.Lock() - self.thread_map = IDMap() - self.frame_map = IDMap() - self.var_map = IDMap() - self.bp_map = IDMap() - self.source_map = IDMap() - self.enable_source_references = False - self.next_var_ref = 0 - self.exceptions_mgr = ExceptionsManager(self) - self.modules_mgr = ModulesManager(self) - self.internals_filter = InternalsFilter() - - # adapter state self.readylock = threading.Lock() self.readylock.acquire() # Unlock at the end of start(). - self.disconnect_request = None - self.debug_options = {} - self.disconnect_request_event = threading.Event() - self._exited = False - self.path_casing = PathUnNormcase() - self.start_reason = None def start(self, threadname): # event loop - self.loop = futures.EventLoop() - self.event_loop_thread = threading.Thread( - target=self.loop.run_forever, - name='ptvsd.EventLoop', - ) - self.event_loop_thread.pydev_do_not_trace = True - self.event_loop_thread.is_pydev_daemon_thread = True - self.event_loop_thread.daemon = True - self.event_loop_thread.start() + self._start_event_loop() # VSC msg processing loop def process_messages(): @@ -797,8 +846,6 @@ class VSCodeMessageProcessor(ipcjson.SocketIO, ipcjson.IpcChannel): debug('output sent') self.readylock.release() - # closing the adapter - def close(self): """Stop the message processor and release its resources.""" debug('raw closing') @@ -810,10 +857,27 @@ class VSCodeMessageProcessor(ipcjson.SocketIO, ipcjson.IpcChannel): # Close the editor-side socket. self._stop_vsc_message_loop() + # VSC protocol handlers + + def send_error_response(self, request, message=None): + self.send_response( + request, + success=False, + message=message + ) + + # internal methods + + def _wait_for_server_thread(self): + if self.server_thread is None: + return + if not self.server_thread.is_alive(): + return + self.server_thread.join(WAIT_FOR_THREAD_FINISH_TIMEOUT) + def _stop_vsc_message_loop(self): self.set_exit() - self.loop.stop() - self.event_loop_thread.join(WAIT_FOR_THREAD_FINISH_TIMEOUT) + self._stop_event_loop() if self.socket: try: self.socket.shutdown(socket.SHUT_RDWR) @@ -822,16 +886,66 @@ class VSCodeMessageProcessor(ipcjson.SocketIO, ipcjson.IpcChannel): # TODO: log the error pass - def _wait_options(self): - # In attach scenarios, we can't assume that the process is actually - # interactive and has a console, so ignore these options. - # In launch scenarios, we only want "press any key" to show up when - # program terminates by itself, not when user explicitly stops it. - if self.disconnect_request or self.start_reason != 'launch': - return False, False - normal = self.debug_options.get('WAIT_ON_NORMAL_EXIT', False) - abnormal = self.debug_options.get('WAIT_ON_ABNORMAL_EXIT', False) - return normal, abnormal + # methods for subclasses to override + + def _start_event_loop(self): + pass + + def _stop_event_loop(self): + pass + + +INITIALIZE_RESPONSE = dict( + supportsExceptionInfoRequest=True, + supportsConfigurationDoneRequest=True, + supportsConditionalBreakpoints=True, + supportsHitConditionalBreakpoints=True, + supportsSetVariable=True, + supportsExceptionOptions=True, + supportsEvaluateForHovers=True, + supportsValueFormattingOptions=True, + supportsSetExpression=True, + supportsModulesRequest=True, + supportsLogPoints=True, + supportTerminateDebuggee=True, + exceptionBreakpointFilters=[ + { + 'filter': 'raised', + 'label': 'Raised Exceptions', + 'default': False + }, + { + 'filter': 'uncaught', + 'label': 'Uncaught Exceptions', + 'default': True + }, + ], +) + + +class VSCLifecycleMsgProcessor(VSCodeMessageProcessorBase): + """Handles adapter lifecycle messages of the VSC debugger protocol.""" + + def __init__(self, socket, + notify_disconnecting, notify_closing, notify_launch=None, + timeout=None, logfile=None, + ): + super(VSCLifecycleMsgProcessor, self).__init__( + socket=socket, + notify_closing=notify_closing, + timeout=timeout, + logfile=logfile, + ) + self._notify_launch = notify_launch or (lambda: None) + self._notify_disconnecting = notify_disconnecting + + self._exited = False + + # adapter state + self.disconnect_request = None + self.debug_options = {} + self.disconnect_request_event = threading.Event() + self.start_reason = None def handle_session_stopped(self, exitcode=None): """Finalize the protocol connection.""" @@ -848,6 +962,52 @@ class VSCodeMessageProcessor(ipcjson.SocketIO, ipcjson.IpcChannel): # The editor will send a "disconnect" request at this point. self._wait_for_disconnect() + # VSC protocol handlers + + def on_initialize(self, request, args): + # TODO: docstring + self.send_response(request, **INITIALIZE_RESPONSE) + self.send_event('initialized') + + def on_configurationDone(self, request, args): + # TODO: docstring + self.send_response(request) + self._process_debug_options(self.debug_options) + self._handle_configurationDone(args) + + def on_attach(self, request, args): + # TODO: docstring + self.start_reason = 'attach' + self._set_debug_options(args) + self._handle_attach(args) + self.send_response(request) + + def on_launch(self, request, args): + # TODO: docstring + self.start_reason = 'launch' + self._set_debug_options(args) + self._notify_launch() + self._handle_launch(args) + self.send_response(request) + + def on_disconnect(self, request, args): + # TODO: docstring + if self.start_reason == 'launch': + self._handle_disconnect(request) + else: + self.send_response(request) + self._notify_disconnecting(kill=False) + + # internal methods + + def _set_debug_options(self, args): + self.debug_options = _extract_debug_options( + args.get('options'), + args.get('debugOptions'), + ) + + # methods related to shutdown + def _wait_for_disconnect(self, timeout=None): if timeout is None: timeout = WAIT_FOR_DISCONNECT_REQUEST_TIMEOUT @@ -859,6 +1019,7 @@ class VSCodeMessageProcessor(ipcjson.SocketIO, ipcjson.IpcChannel): self.disconnect_request = None def _handle_disconnect(self, request): + assert self.start_reason == 'launch' self.disconnect_request = request self.disconnect_request_event.set() self._notify_disconnecting(kill=not self._closed) @@ -867,12 +1028,92 @@ class VSCodeMessageProcessor(ipcjson.SocketIO, ipcjson.IpcChannel): # so just terminate the process altogether. sys.exit(0) - def _wait_for_server_thread(self): - if self.server_thread is None: - return - if not self.server_thread.is_alive(): - return - self.server_thread.join(WAIT_FOR_THREAD_FINISH_TIMEOUT) + def _wait_options(self): + # In attach scenarios, we can't assume that the process is actually + # interactive and has a console, so ignore these options. + # In launch scenarios, we only want "press any key" to show up when + # program terminates by itself, not when user explicitly stops it. + if self.disconnect_request or self.start_reason != 'launch': + return False, False + normal = self.debug_options.get('WAIT_ON_NORMAL_EXIT', False) + abnormal = self.debug_options.get('WAIT_ON_ABNORMAL_EXIT', False) + return normal, abnormal + + # methods for subclasses to override + + def _process_debug_options(self, opts): + pass + + def _handle_configurationDone(self, args): + pass + + def _handle_attach(self, args): + pass + + def _handle_launch(self, args): + pass + + +class VSCodeMessageProcessor(VSCLifecycleMsgProcessor): + """IPC JSON message processor for VSC debugger protocol. + + This translates between the VSC debugger protocol and the pydevd + protocol. + """ + + def __init__(self, socket, pydevd_notify, pydevd_request, + notify_disconnecting, notify_closing, + timeout=None, logfile=None, + ): + super(VSCodeMessageProcessor, self).__init__( + socket=socket, + notify_disconnecting=notify_disconnecting, + notify_closing=notify_closing, + timeout=timeout, + logfile=logfile, + ) + self._pydevd_notify = pydevd_notify + self._pydevd_request = pydevd_request + + self.loop = None + self.event_loop_thread = None + self.bkpoints = None + + # debugger state + self.is_process_created = False + self.is_process_created_lock = threading.Lock() + self.stack_traces = {} + self.stack_traces_lock = threading.Lock() + self.active_exceptions = {} + self.active_exceptions_lock = threading.Lock() + self.thread_map = IDMap() + self.frame_map = IDMap() + self.var_map = IDMap() + self.bp_map = IDMap() + self.source_map = IDMap() + self.enable_source_references = False + self.next_var_ref = 0 + self.exceptions_mgr = ExceptionsManager(self) + self.modules_mgr = ModulesManager(self) + self.internals_filter = InternalsFilter() + + # adapter state + self.path_casing = PathUnNormcase() + + def _start_event_loop(self): + self.loop = futures.EventLoop() + self.event_loop_thread = threading.Thread( + target=self.loop.run_forever, + name='ptvsd.EventLoop', + ) + self.event_loop_thread.pydev_do_not_trace = True + self.event_loop_thread.is_pydev_daemon_thread = True + self.event_loop_thread.daemon = True + self.event_loop_thread.start() + + def _stop_event_loop(self): + self.loop.stop() + self.event_loop_thread.join(WAIT_FOR_THREAD_FINISH_TIMEOUT) # async helpers @@ -961,17 +1202,7 @@ class VSCodeMessageProcessor(ipcjson.SocketIO, ipcjson.IpcChannel): # VSC protocol handlers - @async_handler - def on_initialize(self, request, args): - # TODO: docstring - self.send_response(request, **INITIALIZE_RESPONSE) - self.send_event('initialized') - - @async_handler - def on_configurationDone(self, request, args): - # TODO: docstring - self.send_response(request) - self.process_debug_options() + def _handle_configurationDone(self, args): self.pydevd_request(pydevd_comm.CMD_RUN, '') if self.start_reason == 'attach': @@ -983,92 +1214,17 @@ class VSCodeMessageProcessor(ipcjson.SocketIO, ipcjson.IpcChannel): self.is_process_created = True self.send_process_event(self.start_reason) - def process_debug_options(self): - """ - Process the launch arguments to configure the debugger. - """ # noqa - if self.debug_options.get('FIX_FILE_PATH_CASE', False): + def _process_debug_options(self, opts): + """Process the launch arguments to configure the debugger.""" + if opts.get('FIX_FILE_PATH_CASE', False): self.path_casing.enable() - if self.debug_options.get('REDIRECT_OUTPUT', False): + if opts.get('REDIRECT_OUTPUT', False): redirect_output = 'STDOUT\tSTDERR' else: redirect_output = '' self.pydevd_request(pydevd_comm.CMD_REDIRECT_OUTPUT, redirect_output) - def build_debug_options(self, debug_options): - """ - Build string representation of debug options from launch config (as provided by VSC) - Further information can be found here https://code.visualstudio.com/docs/editor/debugging#_launchjson-attributes - { - type:'python', - request:'launch'|'attach', - name:'friendly name for debug config', - debugOptions:[ - 'RedirectOutput', 'Django' - ] - } - """ # noqa - debug_option_mapping = { - 'RedirectOutput': 'REDIRECT_OUTPUT=True', - 'WaitOnNormalExit': 'WAIT_ON_NORMAL_EXIT=True', - 'WaitOnAbnormalExit': 'WAIT_ON_ABNORMAL_EXIT=True', - 'Django': 'DJANGO_DEBUG=True', - 'Flask': 'FLASK_DEBUG=True', - 'Jinja': 'FLASK_DEBUG=True', - 'FixFilePathCase': 'FIX_FILE_PATH_CASE=True', - 'DebugStdLib': 'DEBUG_STDLIB=True', - 'WindowsClient': 'WINDOWS_CLIENT=True', - } - return ';'.join(debug_option_mapping[option] - for option in debug_options - if option in debug_option_mapping) - - def _parse_debug_options(self, debug_options): - """Debug options are semicolon separated key=value pairs - WAIT_ON_ABNORMAL_EXIT=True|False - WAIT_ON_NORMAL_EXIT=True|False - REDIRECT_OUTPUT=True|False - VERSION=string - INTERPRETER_OPTIONS=string - WEB_BROWSER_URL=string url - DJANGO_DEBUG=True|False - WINDOWS_CLIENT=True|False - DEBUG_STDLIB=True|False - """ - def bool_parser(str): - return str in ("True", "true", "1") - - DEBUG_OPTIONS_PARSER = { - 'WAIT_ON_ABNORMAL_EXIT': bool_parser, - 'WAIT_ON_NORMAL_EXIT': bool_parser, - 'REDIRECT_OUTPUT': bool_parser, - 'VERSION': unquote, - 'INTERPRETER_OPTIONS': unquote, - 'WEB_BROWSER_URL': unquote, - 'DJANGO_DEBUG': bool_parser, - 'FLASK_DEBUG': bool_parser, - 'FIX_FILE_PATH_CASE': bool_parser, - 'WINDOWS_CLIENT': bool_parser, - 'DEBUG_STDLIB': bool_parser, - } - - options = {} - for opt in debug_options.split(';'): - try: - key, value = opt.split('=') - except ValueError: - continue - try: - options[key] = DEBUG_OPTIONS_PARSER[key](value) - except KeyError: - continue - - if 'WINDOWS_CLIENT' not in options: - options['WINDOWS_CLIENT'] = platform.system() == 'Windows' # noqa - - return options - def _initialize_path_maps(self, args): pathMaps = [] for pathMapping in args.get('pathMappings', []): @@ -1090,34 +1246,14 @@ class VSCodeMessageProcessor(ipcjson.SocketIO, ipcjson.IpcChannel): return self.pydevd_request(cmd, msg) @async_handler - def on_attach(self, request, args): - # TODO: docstring - self.start_reason = 'attach' + def _handle_attach(self, args): self._initialize_path_maps(args) - options = self.build_debug_options(args.get('debugOptions', [])) - self.debug_options = self._parse_debug_options( - args.get('options', options)) yield self._send_cmd_version_command() - self.send_response(request) @async_handler - def on_launch(self, request, args): - # TODO: docstring - self.start_reason = 'launch' + def _handle_launch(self, args): self._initialize_path_maps(args) - options = self.build_debug_options(args.get('debugOptions', [])) - self.debug_options = self._parse_debug_options( - args.get('options', options)) yield self._send_cmd_version_command() - self.send_response(request) - - def on_disconnect(self, request, args): - # TODO: docstring - if self.start_reason == 'launch': - self._handle_disconnect(request) - else: - self.send_response(request) - self._notify_disconnecting(kill=False) def send_process_event(self, start_method): # TODO: docstring @@ -1129,21 +1265,6 @@ class VSCodeMessageProcessor(ipcjson.SocketIO, ipcjson.IpcChannel): } self.send_event('process', **evt) - def send_error_response(self, request, message=None): - self.send_response( - request, - success=False, - message=message - ) - - def is_debugger_internal_thread(self, thread_name): - if thread_name: - if thread_name.startswith('pydevd.'): - return True - elif thread_name.startswith('ptvsd.'): - return True - return False - @async_handler def on_threads(self, request, args): # TODO: docstring @@ -1168,7 +1289,7 @@ class VSCodeMessageProcessor(ipcjson.SocketIO, ipcjson.IpcChannel): except KeyError: name = None - if not self.is_debugger_internal_thread(name): + if not is_debugger_internal_thread(name): pyd_tid = xthread['id'] try: vsc_tid = self.thread_map.to_vscode(pyd_tid, autogen=False) @@ -1880,7 +2001,7 @@ class VSCodeMessageProcessor(ipcjson.SocketIO, ipcjson.IpcChannel): name = unquote(xml.thread['name']) except KeyError: name = None - if not self.is_debugger_internal_thread(name): + if not is_debugger_internal_thread(name): # Any internal pydevd or ptvsd threads will be ignored everywhere tid = self.thread_map.to_vscode(xml.thread['id'], autogen=True) self.send_event('thread', reason='started', threadId=tid) diff --git a/tests/helpers/debugclient.py b/tests/helpers/debugclient.py index f40ab074..2be79d00 100644 --- a/tests/helpers/debugclient.py +++ b/tests/helpers/debugclient.py @@ -189,6 +189,8 @@ class EasyDebugClient(DebugClient): argv = [ filename, ] + list(argv) + if kwargs.pop('nodebug', False): + argv.insert(0, '--nodebug') self._launch(argv, **kwargs) return self._adapter, self._session @@ -202,5 +204,7 @@ class EasyDebugClient(DebugClient): argv = [ '-m', module, ] + list(argv) + if kwargs.pop('nodebug', False): + argv.insert(0, '--nodebug') self._launch(argv, **kwargs) return self._adapter, self._session diff --git a/tests/system_tests/test_main.py b/tests/system_tests/test_main.py index 0e25538d..04c6b1a9 100644 --- a/tests/system_tests/test_main.py +++ b/tests/system_tests/test_main.py @@ -471,3 +471,40 @@ class LifecycleTests(TestsBase, unittest.TestCase): self.new_event('exited', exitCode=0), self.new_event('terminated'), ]) + + def test_nodebug(self): + lockfile = self.workspace.lockfile() + done, waitscript = lockfile.wait_in_script() + filename = self.write_script('spam.py', dedent(""" + print('+ before') + + {} + + print('+ after') + """).format(waitscript)) + with DebugClient(port=9876) as editor: + adapter, session = editor.host_local_debugger( + argv=[ + '--nodebug', + filename, + ], + ) + + (req_initialize, req_launch, req_config + ) = lifecycle_handshake(session, 'launch') + + done() + adapter.wait() + + self.assert_received(session.received, [ + self.new_version_event(session.received), + self.new_response(req_initialize, **INITIALIZE_RESPONSE), + self.new_event('initialized'), + self.new_response(req_launch), + self.new_response(req_config), + self.new_event('output', + output='+ before\n+ after\n', + category='stdout'), + self.new_event('exited', exitCode=0), + self.new_event('terminated'), + ])